@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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ContactRead } from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
2
|
+
|
|
2
3
|
import { cliIpcCall } from "../../../../ipc/cli-client.js";
|
|
3
4
|
import { resolveGuardianName } from "../../../../prompts/user-reference.js";
|
|
4
5
|
import type {
|
|
@@ -6,6 +7,12 @@ import type {
|
|
|
6
7
|
ToolExecutionResult,
|
|
7
8
|
} from "../../../../tools/types.js";
|
|
8
9
|
|
|
10
|
+
function guardianAwareName(contact: Pick<ContactRead, "role" | "displayName">) {
|
|
11
|
+
return contact.role === "guardian"
|
|
12
|
+
? resolveGuardianName(contact.displayName)
|
|
13
|
+
: contact.displayName;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
export async function executeContactMerge(
|
|
10
17
|
input: Record<string, unknown>,
|
|
11
18
|
_context: ToolContext,
|
|
@@ -22,10 +29,10 @@ export async function executeContactMerge(
|
|
|
22
29
|
|
|
23
30
|
// Validate both contacts exist before merging
|
|
24
31
|
const [keepRes, mergeRes] = await Promise.all([
|
|
25
|
-
cliIpcCall<{ contact:
|
|
32
|
+
cliIpcCall<{ contact: ContactRead }>("getContact", {
|
|
26
33
|
pathParams: { id: keepId },
|
|
27
34
|
}),
|
|
28
|
-
cliIpcCall<{ contact:
|
|
35
|
+
cliIpcCall<{ contact: ContactRead }>("getContact", {
|
|
29
36
|
pathParams: { id: mergeId },
|
|
30
37
|
}),
|
|
31
38
|
]);
|
|
@@ -42,7 +49,7 @@ export async function executeContactMerge(
|
|
|
42
49
|
|
|
43
50
|
const mergeResult = await cliIpcCall<{
|
|
44
51
|
ok: boolean;
|
|
45
|
-
contact:
|
|
52
|
+
contact: ContactRead;
|
|
46
53
|
}>("merge_contacts", {
|
|
47
54
|
body: { keepId, mergeId },
|
|
48
55
|
});
|
|
@@ -51,19 +58,22 @@ export async function executeContactMerge(
|
|
|
51
58
|
return { content: `Error: ${mergeResult.error}`, isError: true };
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
const mergedId = mergeResult.result!.contact.id;
|
|
62
|
+
|
|
63
|
+
// Re-read the surviving contact through the gateway-relayed read so role and
|
|
64
|
+
// interactionCount come from the gateway ContactRead.
|
|
65
|
+
const mergedRes = await cliIpcCall<{ contact: ContactRead }>("getContact", {
|
|
66
|
+
pathParams: { id: mergedId },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!mergedRes.ok) {
|
|
70
|
+
return { content: `Error: ${mergedRes.error}`, isError: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const merged = mergedRes.result!.contact;
|
|
74
|
+
const displayName = guardianAwareName(merged);
|
|
75
|
+
const keepName = guardianAwareName(keepContact);
|
|
76
|
+
const mergeName = guardianAwareName(mergeContact);
|
|
67
77
|
|
|
68
78
|
const channelList = merged.channels
|
|
69
79
|
.map(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ContactRead } from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
2
|
+
|
|
2
3
|
import { cliIpcCall } from "../../../../ipc/cli-client.js";
|
|
3
4
|
import { resolveGuardianName } from "../../../../prompts/user-reference.js";
|
|
4
5
|
import type {
|
|
@@ -6,7 +7,16 @@ import type {
|
|
|
6
7
|
ToolExecutionResult,
|
|
7
8
|
} from "../../../../tools/types.js";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
// The search route may carry an optional per-channel `externalChatId` not modeled
|
|
11
|
+
// on the gateway `ContactRead` channel contract.
|
|
12
|
+
type SearchChannel = ContactRead["channels"][number] & {
|
|
13
|
+
externalChatId?: string | null;
|
|
14
|
+
};
|
|
15
|
+
type SearchContact = Omit<ContactRead, "channels"> & {
|
|
16
|
+
channels: SearchChannel[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function formatContactSummary(c: SearchContact): string {
|
|
10
20
|
const displayName =
|
|
11
21
|
c.role === "guardian" ? resolveGuardianName(c.displayName) : c.displayName;
|
|
12
22
|
const parts = [`- **${displayName}** (ID: ${c.id})`];
|
|
@@ -45,7 +55,7 @@ export async function executeContactSearch(
|
|
|
45
55
|
};
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
const res = await cliIpcCall<
|
|
58
|
+
const res = await cliIpcCall<SearchContact[]>("search_contacts", {
|
|
49
59
|
body: { query, channelAddress, channelType, limit },
|
|
50
60
|
});
|
|
51
61
|
|
|
@@ -66,14 +66,6 @@
|
|
|
66
66
|
"description": "Gate per-turn conversation trace (user/assistant/tool-call/tool-response) collection. The daemon attaches a trace to its turn telemetry only when this flag AND the owner's share_diagnostics consent are both on. Defaults off (fail-closed) — the live value is delivered from LaunchDarkly.",
|
|
67
67
|
"defaultEnabled": false
|
|
68
68
|
},
|
|
69
|
-
{
|
|
70
|
-
"id": "workspace-tools-watcher",
|
|
71
|
-
"scope": "assistant",
|
|
72
|
-
"key": "workspace-tools-watcher",
|
|
73
|
-
"label": "Workspace tools file watcher",
|
|
74
|
-
"description": "Hot-reload workspace tool overrides under `<workspaceDir>/tools/` by watching the directory and re-registering tools as files change. When disabled, workspace tools still load from disk once at daemon startup, but live edits require a restart to take effect.",
|
|
75
|
-
"defaultEnabled": false
|
|
76
|
-
},
|
|
77
69
|
{
|
|
78
70
|
"id": "settings-developer-nav",
|
|
79
71
|
"scope": "assistant",
|
package/src/config/loader.ts
CHANGED
|
@@ -300,14 +300,20 @@ function validateWithSchema(raw: Record<string, unknown>): AssistantConfig {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
// Strip invalid fields by setting them to undefined so Zod defaults apply,
|
|
303
|
-
// then re-parse. We walk the error paths and delete the offending keys
|
|
303
|
+
// then re-parse. We walk the error paths and delete the offending keys,
|
|
304
|
+
// pruning any ancestor object the deletion leaves empty. Pruning matters for
|
|
305
|
+
// nested overrides like `llm.callSites.<id>.profile`: stripping just the
|
|
306
|
+
// invalid `.profile` leaf would leave `llm.callSites.<id> = {}`, which the
|
|
307
|
+
// resolver treats as a present (non-default) override and so skips the
|
|
308
|
+
// shipped call-site default — silently downgrading the call site to the
|
|
309
|
+
// active profile. Removing the emptied object lets that default apply.
|
|
304
310
|
const cleaned = structuredClone(raw);
|
|
305
311
|
for (const issue of result.error.issues) {
|
|
306
312
|
if (issue.path.length === 0) {
|
|
307
313
|
// Top-level error — return full defaults
|
|
308
314
|
return cloneDefaultConfig();
|
|
309
315
|
}
|
|
310
|
-
deleteNestedKey(cleaned, issue.path as (string | number)[]);
|
|
316
|
+
deleteNestedKey(cleaned, issue.path as (string | number)[], true);
|
|
311
317
|
}
|
|
312
318
|
|
|
313
319
|
const retry = AssistantConfigSchema.safeParse(cleaned);
|
|
@@ -320,17 +326,42 @@ function validateWithSchema(raw: Record<string, unknown>): AssistantConfig {
|
|
|
320
326
|
return cloneDefaultConfig();
|
|
321
327
|
}
|
|
322
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Delete the key at `path` from `obj`. When `pruneEmptyAncestors` is set, also
|
|
331
|
+
* remove any ancestor object the deletion leaves empty, walking up until the
|
|
332
|
+
* first ancestor that still holds other keys. Only empty plain objects are
|
|
333
|
+
* pruned (arrays are left alone), and a still-populated ancestor stops the walk
|
|
334
|
+
* so a container holding other config is never removed.
|
|
335
|
+
*/
|
|
323
336
|
function deleteNestedKey(
|
|
324
337
|
obj: Record<string, unknown>,
|
|
325
338
|
path: (string | number)[],
|
|
339
|
+
pruneEmptyAncestors = false,
|
|
326
340
|
): void {
|
|
341
|
+
// Record each (container, key) hop on the way down so we can prune upward
|
|
342
|
+
// after deleting the leaf.
|
|
343
|
+
const chain: Array<{ container: Record<string, unknown>; key: string }> = [];
|
|
327
344
|
let current: unknown = obj;
|
|
328
345
|
for (let i = 0; i < path.length - 1; i++) {
|
|
329
346
|
if (current == null || typeof current !== "object") return;
|
|
330
|
-
|
|
347
|
+
const key = String(path[i]);
|
|
348
|
+
chain.push({ container: current as Record<string, unknown>, key });
|
|
349
|
+
current = (current as Record<string, unknown>)[key];
|
|
331
350
|
}
|
|
332
|
-
if (current
|
|
333
|
-
|
|
351
|
+
if (current == null || typeof current !== "object") return;
|
|
352
|
+
delete (current as Record<string, unknown>)[String(path[path.length - 1])];
|
|
353
|
+
|
|
354
|
+
if (!pruneEmptyAncestors) return;
|
|
355
|
+
// Remove ancestors emptied by the deletion, deepest first; stop at the first
|
|
356
|
+
// that still has keys.
|
|
357
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
358
|
+
const { container, key } = chain[i];
|
|
359
|
+
const child = container[key];
|
|
360
|
+
if (isPlainObject(child) && Object.keys(child).length === 0) {
|
|
361
|
+
delete container[key];
|
|
362
|
+
} else {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
334
365
|
}
|
|
335
366
|
}
|
|
336
367
|
|
|
@@ -78,6 +78,17 @@ export const MemoryJobsConfigSchema = MemoryJobsConfigInputSchema.transform(
|
|
|
78
78
|
},
|
|
79
79
|
).describe("Memory background job processing configuration");
|
|
80
80
|
|
|
81
|
+
export const MemoryWorkerConfigSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
enabled: z
|
|
84
|
+
.boolean({ error: "memory.worker.enabled must be a boolean" })
|
|
85
|
+
.default(false)
|
|
86
|
+
.describe(
|
|
87
|
+
"Whether the memory jobs worker runs as a separate OS process spawned at assistant startup (the `assistant memory worker` implementation) instead of on the assistant's main event loop. Only affects startup; shutdown stops whichever worker is actually running.",
|
|
88
|
+
),
|
|
89
|
+
})
|
|
90
|
+
.describe("Memory jobs worker process configuration");
|
|
91
|
+
|
|
81
92
|
export const MemoryRetentionConfigSchema = z
|
|
82
93
|
.object({
|
|
83
94
|
keepRawForever: z
|
|
@@ -185,6 +196,7 @@ export const MemoryMaintenanceConfigSchema = z
|
|
|
185
196
|
);
|
|
186
197
|
|
|
187
198
|
export type MemoryJobsConfig = z.infer<typeof MemoryJobsConfigSchema>;
|
|
199
|
+
export type MemoryWorkerConfig = z.infer<typeof MemoryWorkerConfigSchema>;
|
|
188
200
|
export type MemoryRetentionConfig = z.infer<typeof MemoryRetentionConfigSchema>;
|
|
189
201
|
export type MemoryCleanupConfig = z.infer<typeof MemoryCleanupConfigSchema>;
|
|
190
202
|
export type MemoryMaintenanceConfig = z.infer<
|
|
@@ -259,6 +259,13 @@ export const MemoryV3ConfigSchema = z
|
|
|
259
259
|
.describe(
|
|
260
260
|
"Per-lane article budget for the reply-query finder pass: needle and dense each re-run over the assistant's previous message as separate queries (never concatenated with the user's message). 0 disables the pass. Deliberately small next to needleK/denseK — the pass adds the assistant-side retrieval signal, not a second full sweep.",
|
|
261
261
|
),
|
|
262
|
+
selectorPromptPath: z
|
|
263
|
+
.string({ error: "memory.v3.selectorPromptPath must be a string" })
|
|
264
|
+
.nullable()
|
|
265
|
+
.default(null)
|
|
266
|
+
.describe(
|
|
267
|
+
"Optional path to a file whose contents replace the bundled per-turn selector system prompt (the instructions that tell the selector which candidate pages to keep). Absolute paths are used as-is, a leading `~/` is expanded to the home directory, otherwise the path is resolved under the workspace root. The selector prompt takes no placeholders — the candidate pool is supplied separately as the user message — so the file is used verbatim. If the file is missing, unreadable, empty, or over 1 MiB, the bundled prompt is used and a warning is logged.",
|
|
268
|
+
),
|
|
262
269
|
edge: MemoryV3EdgeSchema.default(MemoryV3EdgeSchema.parse({})),
|
|
263
270
|
})
|
|
264
271
|
.describe("Memory v3 — section-grain lane retrieval");
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
MemoryJobsConfigSchema,
|
|
6
6
|
MemoryMaintenanceConfigSchema,
|
|
7
7
|
MemoryRetentionConfigSchema,
|
|
8
|
+
MemoryWorkerConfigSchema,
|
|
8
9
|
} from "./memory-lifecycle.js";
|
|
9
10
|
import {
|
|
10
11
|
MemoryExtractionConfigSchema,
|
|
@@ -39,6 +40,9 @@ export const MemoryConfigSchema = z
|
|
|
39
40
|
MemorySegmentationConfigSchema.parse({}),
|
|
40
41
|
),
|
|
41
42
|
jobs: MemoryJobsConfigSchema.default(MemoryJobsConfigSchema.parse({})),
|
|
43
|
+
worker: MemoryWorkerConfigSchema.default(
|
|
44
|
+
MemoryWorkerConfigSchema.parse({}),
|
|
45
|
+
),
|
|
42
46
|
retention: MemoryRetentionConfigSchema.default(
|
|
43
47
|
MemoryRetentionConfigSchema.parse({}),
|
|
44
48
|
),
|
|
@@ -28,6 +28,14 @@ export const TimeoutConfigSchema = z
|
|
|
28
28
|
.describe(
|
|
29
29
|
"How long to wait for user permission approval before timing out (seconds)",
|
|
30
30
|
),
|
|
31
|
+
questionResponseTimeoutSec: z
|
|
32
|
+
.number({ error: "timeouts.questionResponseTimeoutSec must be a number" })
|
|
33
|
+
.finite("timeouts.questionResponseTimeoutSec must be finite")
|
|
34
|
+
.positive("timeouts.questionResponseTimeoutSec must be a positive number")
|
|
35
|
+
.default(1800)
|
|
36
|
+
.describe(
|
|
37
|
+
"Backstop timeout for an unanswered ask_question prompt (seconds). The primary way an interactive user dismisses a prompt is by moving on — enqueuing another message supersedes it — so this only bounds a prompt left open with no response and no follow-up message.",
|
|
38
|
+
),
|
|
31
39
|
toolExecutionTimeoutSec: z
|
|
32
40
|
.number({ error: "timeouts.toolExecutionTimeoutSec must be a number" })
|
|
33
41
|
.finite("timeouts.toolExecutionTimeoutSec must be finite")
|
|
@@ -88,15 +88,24 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
|
|
|
88
88
|
// profile there's nothing stronger to consult, so the advisor defaults off.
|
|
89
89
|
advisorEnabled: false,
|
|
90
90
|
},
|
|
91
|
+
// Served by DeepSeek V4 Flash on Fireworks via managed platform inference: a
|
|
92
|
+
// fast, low-cost open model. `model` is pinned explicitly rather than
|
|
93
|
+
// resolved via the `latency-optimized` intent (which still maps to Kimi K2.5
|
|
94
|
+
// on Fireworks and Anthropic Haiku elsewhere).
|
|
95
|
+
//
|
|
96
|
+
// `effort: "none"` (not "low") because Fireworks is not thinking-aware: the
|
|
97
|
+
// disabled `thinking` config is stripped before the request, so a non-"none"
|
|
98
|
+
// effort would be sent as `reasoning_effort` and make this profile pay for
|
|
99
|
+
// reasoning despite thinking being off. "none" keeps Speed non-reasoning.
|
|
91
100
|
"cost-optimized": {
|
|
92
|
-
|
|
93
|
-
provider: "
|
|
94
|
-
connectionName: "
|
|
101
|
+
model: "accounts/fireworks/models/deepseek-v4-flash",
|
|
102
|
+
provider: "fireworks",
|
|
103
|
+
connectionName: "fireworks-managed",
|
|
95
104
|
source: "managed",
|
|
96
105
|
label: "Speed",
|
|
97
|
-
description: "Fastest responses at lower cost",
|
|
106
|
+
description: "Fastest responses at lower cost (DeepSeek V4 Flash)",
|
|
98
107
|
maxTokens: 8192,
|
|
99
|
-
effort: "
|
|
108
|
+
effort: "none",
|
|
100
109
|
thinking: { enabled: false, streamThinking: false },
|
|
101
110
|
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
102
111
|
},
|
package/src/config/skills.ts
CHANGED
|
@@ -688,6 +688,9 @@ function discoverSkillDirectories(skillsDir: string): string[] {
|
|
|
688
688
|
* parseable `package.json` whose `name` equals the directory name. This
|
|
689
689
|
* mirrors the external plugin loader's recognition gate, which skips any
|
|
690
690
|
* directory whose `manifest.name` does not match its directory name.
|
|
691
|
+
*
|
|
692
|
+
* The caller is responsible for the missing-`package.json` case (it emits a
|
|
693
|
+
* diagnostic warning); this function only judges a manifest that is present.
|
|
691
694
|
*/
|
|
692
695
|
function isRecognizedPluginDir(pluginDir: string, dirName: string): boolean {
|
|
693
696
|
const manifestPath = join(pluginDir, "package.json");
|
|
@@ -735,12 +738,31 @@ function discoverPluginResidentSkills(): SkillSummary[] {
|
|
|
735
738
|
for (const entry of entries) {
|
|
736
739
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
737
740
|
const pluginDir = join(pluginsDir, entry.name);
|
|
741
|
+
|
|
742
|
+
// A directory under `plugins/` with no `package.json` is not a plugin the
|
|
743
|
+
// runtime can load, so its skills are never surfaced. This is an easy
|
|
744
|
+
// footgun — a plugin dropped in without its manifest looks installed but
|
|
745
|
+
// silently contributes nothing — so warn loudly with the path rather than
|
|
746
|
+
// skipping in silence, to make the misconfiguration diagnosable.
|
|
747
|
+
if (!existsSync(join(pluginDir, "package.json"))) {
|
|
748
|
+
log.warn(
|
|
749
|
+
{ pluginDir },
|
|
750
|
+
"Plugin directory is missing package.json — skipping; its skills will not be available. Add a package.json whose `name` matches the directory.",
|
|
751
|
+
);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Honor the `.disabled` sentinel the runtime plugin scan checks
|
|
756
|
+
// (`plugins/mtime-cache.ts`): a disabled plugin contributes no hooks or
|
|
757
|
+
// tools, so its resident skills must not be loadable either.
|
|
758
|
+
if (existsSync(join(pluginDir, ".disabled"))) continue;
|
|
759
|
+
|
|
738
760
|
// Mirror the plugin loader's recognition gate: a directory is a real
|
|
739
|
-
// installed plugin only if
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
761
|
+
// installed plugin only if its `package.json` `name` matches the directory.
|
|
762
|
+
// This rejects staging dirs and malformed/mismatched clones (e.g. an
|
|
763
|
+
// un-adapted `caveman-installer`) that the loader itself would skip, so the
|
|
764
|
+
// catalog never surfaces skills from a directory the runtime would refuse
|
|
765
|
+
// to load.
|
|
744
766
|
if (!isRecognizedPluginDir(pluginDir, entry.name)) continue;
|
|
745
767
|
|
|
746
768
|
const skillsDir = join(pluginDir, "skills");
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the gateway-backed guardian delivery reader.
|
|
3
|
+
*
|
|
4
|
+
* Guardian binding is near-static, so the reader caches behind a minutes-scale
|
|
5
|
+
* TTL, clears event-driven on invalidation, and coalesces concurrent cold-cache
|
|
6
|
+
* reads single-flight. These tests pin the parse contract plus all three cache
|
|
7
|
+
* behaviors (TTL hit, invalidation, single-flight) and the failure-no-poison
|
|
8
|
+
* rule.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import type { GuardianDelivery } from "@vellumai/gateway-client";
|
|
14
|
+
|
|
15
|
+
// ── Controllable IPC mock ────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
type IpcHandler = (params?: Record<string, unknown>) => unknown;
|
|
18
|
+
|
|
19
|
+
const ipcHandlers = new Map<string, IpcHandler>();
|
|
20
|
+
const ipcCallLog: Array<{
|
|
21
|
+
method: string;
|
|
22
|
+
params?: Record<string, unknown>;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}> = [];
|
|
25
|
+
|
|
26
|
+
mock.module("../../ipc/gateway-client.js", () => ({
|
|
27
|
+
ipcCall: async (
|
|
28
|
+
method: string,
|
|
29
|
+
params?: Record<string, unknown>,
|
|
30
|
+
timeoutMs?: number,
|
|
31
|
+
) => {
|
|
32
|
+
ipcCallLog.push({ method, params, timeoutMs });
|
|
33
|
+
const handler = ipcHandlers.get(method);
|
|
34
|
+
return handler ? handler(params) : undefined;
|
|
35
|
+
},
|
|
36
|
+
ipcCallPersistent: async () => undefined,
|
|
37
|
+
resetPersistentClient: () => {},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import { emitContactChange } from "../contact-events.js";
|
|
41
|
+
import {
|
|
42
|
+
__resetGuardianDeliveryCacheForTest,
|
|
43
|
+
anyGuardian,
|
|
44
|
+
getGuardianDelivery,
|
|
45
|
+
getGuardianDeliveryFresh,
|
|
46
|
+
guardianForChannel,
|
|
47
|
+
invalidateGuardianDeliveryCache,
|
|
48
|
+
} from "../guardian-delivery-reader.js";
|
|
49
|
+
|
|
50
|
+
const METHOD = "resolve_guardian_delivery";
|
|
51
|
+
|
|
52
|
+
function countCalls(method: string): number {
|
|
53
|
+
return ipcCallLog.filter((c) => c.method === method).length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const telegramGuardian: GuardianDelivery = {
|
|
57
|
+
channelType: "telegram",
|
|
58
|
+
contactId: "contact-123",
|
|
59
|
+
address: "@guardian",
|
|
60
|
+
status: "active",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const emailGuardian: GuardianDelivery = {
|
|
64
|
+
channelType: "email",
|
|
65
|
+
contactId: "contact-456",
|
|
66
|
+
address: "guardian@example.com",
|
|
67
|
+
status: "active",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe("getGuardianDelivery", () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
__resetGuardianDeliveryCacheForTest();
|
|
73
|
+
ipcHandlers.clear();
|
|
74
|
+
ipcCallLog.length = 0;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns the parsed guardian list", async () => {
|
|
78
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
79
|
+
|
|
80
|
+
expect(await getGuardianDelivery()).toEqual([telegramGuardian]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("bounds the IPC read with a short timeout", async () => {
|
|
84
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [] }));
|
|
85
|
+
|
|
86
|
+
await getGuardianDelivery();
|
|
87
|
+
|
|
88
|
+
const call = ipcCallLog.find((c) => c.method === METHOD);
|
|
89
|
+
expect(call?.timeoutMs).toBe(2_000);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("returns null when IPC transport fails (undefined)", async () => {
|
|
93
|
+
ipcHandlers.set(METHOD, () => undefined);
|
|
94
|
+
expect(await getGuardianDelivery()).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns null when the IPC call throws", async () => {
|
|
98
|
+
ipcHandlers.set(METHOD, () => {
|
|
99
|
+
throw new Error("socket exploded");
|
|
100
|
+
});
|
|
101
|
+
expect(await getGuardianDelivery()).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("returns null for a malformed response shape", async () => {
|
|
105
|
+
ipcHandlers.set(METHOD, () => ({ guardians: "not-an-array" }));
|
|
106
|
+
expect(await getGuardianDelivery()).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("two calls within the TTL issue only ONE IPC call (cache hit)", async () => {
|
|
110
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
111
|
+
|
|
112
|
+
await getGuardianDelivery();
|
|
113
|
+
await getGuardianDelivery();
|
|
114
|
+
|
|
115
|
+
expect(countCalls(METHOD)).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("caches per channelTypes filter key", async () => {
|
|
119
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
120
|
+
|
|
121
|
+
await getGuardianDelivery();
|
|
122
|
+
await getGuardianDelivery({ channelTypes: ["telegram"] });
|
|
123
|
+
|
|
124
|
+
// Distinct keys ("ALL" vs "telegram") miss each other → two IPC calls.
|
|
125
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("invalidateGuardianDeliveryCache() forces the next call to re-fetch", async () => {
|
|
129
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
130
|
+
|
|
131
|
+
await getGuardianDelivery();
|
|
132
|
+
invalidateGuardianDeliveryCache();
|
|
133
|
+
await getGuardianDelivery();
|
|
134
|
+
|
|
135
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a contact-change event clears the cache", async () => {
|
|
139
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
140
|
+
|
|
141
|
+
await getGuardianDelivery();
|
|
142
|
+
emitContactChange();
|
|
143
|
+
await getGuardianDelivery();
|
|
144
|
+
|
|
145
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a burst of concurrent cold-cache calls issues only ONE IPC call (single-flight)", async () => {
|
|
149
|
+
let resolveIpc: ((value: unknown) => void) | undefined;
|
|
150
|
+
ipcHandlers.set(
|
|
151
|
+
METHOD,
|
|
152
|
+
() =>
|
|
153
|
+
new Promise((resolve) => {
|
|
154
|
+
resolveIpc = resolve;
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const burst = Promise.all([
|
|
159
|
+
getGuardianDelivery(),
|
|
160
|
+
getGuardianDelivery(),
|
|
161
|
+
getGuardianDelivery(),
|
|
162
|
+
]);
|
|
163
|
+
resolveIpc?.({ guardians: [telegramGuardian] });
|
|
164
|
+
const results = await burst;
|
|
165
|
+
|
|
166
|
+
expect(countCalls(METHOD)).toBe(1);
|
|
167
|
+
expect(results).toEqual([
|
|
168
|
+
[telegramGuardian],
|
|
169
|
+
[telegramGuardian],
|
|
170
|
+
[telegramGuardian],
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("an invalidation DURING an in-flight fetch is not masked — the next call re-fetches", async () => {
|
|
175
|
+
let resolveIpc: ((value: unknown) => void) | undefined;
|
|
176
|
+
ipcHandlers.set(
|
|
177
|
+
METHOD,
|
|
178
|
+
() =>
|
|
179
|
+
new Promise((resolve) => {
|
|
180
|
+
resolveIpc = resolve;
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Start a cold fetch, invalidate before it resolves, then resolve it.
|
|
185
|
+
const inFlight = getGuardianDelivery();
|
|
186
|
+
invalidateGuardianDeliveryCache();
|
|
187
|
+
resolveIpc?.({ guardians: [telegramGuardian] });
|
|
188
|
+
expect(await inFlight).toEqual([telegramGuardian]);
|
|
189
|
+
|
|
190
|
+
// The pre-invalidation result must NOT have been cached: the next read
|
|
191
|
+
// issues a fresh IPC rather than serving the now-stale value.
|
|
192
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [emailGuardian] }));
|
|
193
|
+
expect(await getGuardianDelivery()).toEqual([emailGuardian]);
|
|
194
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a failure does NOT poison the cache — the next call retries", async () => {
|
|
198
|
+
ipcHandlers.set(METHOD, () => undefined);
|
|
199
|
+
expect(await getGuardianDelivery()).toBeNull();
|
|
200
|
+
|
|
201
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
202
|
+
expect(await getGuardianDelivery()).toEqual([telegramGuardian]);
|
|
203
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("fresh read ignores a stale cached entry and re-fetches", async () => {
|
|
207
|
+
// Seed the cache with an empty list (the stale gateway-side view).
|
|
208
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [] }));
|
|
209
|
+
expect(await getGuardianDelivery()).toEqual([]);
|
|
210
|
+
|
|
211
|
+
// A cached read still serves the stale empty list (no new IPC)...
|
|
212
|
+
expect(await getGuardianDelivery()).toEqual([]);
|
|
213
|
+
expect(countCalls(METHOD)).toBe(1);
|
|
214
|
+
|
|
215
|
+
// ...but a fresh read bypasses the cache and sees the now-present guardian.
|
|
216
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
217
|
+
expect(await getGuardianDeliveryFresh()).toEqual([telegramGuardian]);
|
|
218
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("fresh read updates the cache with the fresh result", async () => {
|
|
222
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [] }));
|
|
223
|
+
await getGuardianDelivery();
|
|
224
|
+
|
|
225
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
226
|
+
await getGuardianDeliveryFresh();
|
|
227
|
+
|
|
228
|
+
// A subsequent cached read serves the refreshed value without a new IPC.
|
|
229
|
+
expect(await getGuardianDelivery()).toEqual([telegramGuardian]);
|
|
230
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("getGuardianDeliveryFresh bypasses a stale cached empty list", async () => {
|
|
234
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [] }));
|
|
235
|
+
expect(
|
|
236
|
+
await getGuardianDelivery({ channelTypes: ["telegram"] }),
|
|
237
|
+
).toEqual([]);
|
|
238
|
+
|
|
239
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
240
|
+
expect(
|
|
241
|
+
await getGuardianDeliveryFresh({ channelTypes: ["telegram"] }),
|
|
242
|
+
).toEqual([telegramGuardian]);
|
|
243
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("a burst of forceRefresh reads still coalesces single-flight", async () => {
|
|
247
|
+
let resolveIpc: ((value: unknown) => void) | undefined;
|
|
248
|
+
ipcHandlers.set(
|
|
249
|
+
METHOD,
|
|
250
|
+
() =>
|
|
251
|
+
new Promise((resolve) => {
|
|
252
|
+
resolveIpc = resolve;
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const burst = Promise.all([
|
|
257
|
+
getGuardianDeliveryFresh(),
|
|
258
|
+
getGuardianDeliveryFresh(),
|
|
259
|
+
getGuardianDeliveryFresh(),
|
|
260
|
+
]);
|
|
261
|
+
resolveIpc?.({ guardians: [telegramGuardian] });
|
|
262
|
+
await burst;
|
|
263
|
+
|
|
264
|
+
expect(countCalls(METHOD)).toBe(1);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("a fresh read does NOT coalesce with an in-flight non-force fetch (issues its own IPC)", async () => {
|
|
268
|
+
// A normal read starts a fetch that will resolve to the pre-write empty
|
|
269
|
+
// list and is still in flight when the fresh read arrives.
|
|
270
|
+
let resolveStale: ((value: unknown) => void) | undefined;
|
|
271
|
+
ipcHandlers.set(
|
|
272
|
+
METHOD,
|
|
273
|
+
() =>
|
|
274
|
+
new Promise((resolve) => {
|
|
275
|
+
resolveStale = resolve;
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
const stale = getGuardianDelivery();
|
|
279
|
+
|
|
280
|
+
// The gateway-side write lands (not reflected in the in-flight fetch). The
|
|
281
|
+
// fresh read must NOT reuse the stale in-flight promise — it issues its own
|
|
282
|
+
// IPC observing the post-write guardian.
|
|
283
|
+
ipcHandlers.set(METHOD, () => ({ guardians: [telegramGuardian] }));
|
|
284
|
+
const fresh = await getGuardianDeliveryFresh();
|
|
285
|
+
expect(fresh).toEqual([telegramGuardian]);
|
|
286
|
+
|
|
287
|
+
// Release the stale fetch last; it must not have masked the fresh result.
|
|
288
|
+
resolveStale?.({ guardians: [] });
|
|
289
|
+
expect(await stale).toEqual([]);
|
|
290
|
+
expect(countCalls(METHOD)).toBe(2);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("selectors", () => {
|
|
295
|
+
test("guardianForChannel picks the first active match for the type", () => {
|
|
296
|
+
const inactive: GuardianDelivery = {
|
|
297
|
+
...telegramGuardian,
|
|
298
|
+
contactId: "contact-999",
|
|
299
|
+
status: "revoked",
|
|
300
|
+
};
|
|
301
|
+
const list = [inactive, telegramGuardian, emailGuardian];
|
|
302
|
+
|
|
303
|
+
expect(guardianForChannel(list, "telegram")).toBe(telegramGuardian);
|
|
304
|
+
expect(guardianForChannel(list, "email")).toBe(emailGuardian);
|
|
305
|
+
expect(guardianForChannel(list, "phone")).toBeUndefined();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("anyGuardian returns the first overall", () => {
|
|
309
|
+
expect(anyGuardian([emailGuardian, telegramGuardian])).toBe(emailGuardian);
|
|
310
|
+
expect(anyGuardian([])).toBeUndefined();
|
|
311
|
+
});
|
|
312
|
+
});
|