@vellumai/assistant 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
package/src/memory/v2/sim.ts
CHANGED
|
@@ -30,7 +30,6 @@ import { applyCorrectionIfCalibrated } from "../anisotropy.js";
|
|
|
30
30
|
import { embedWithBackend } from "../embedding-backend.js";
|
|
31
31
|
import { clampUnitInterval } from "../validation.js";
|
|
32
32
|
import { hybridQueryConceptPages } from "./qdrant.js";
|
|
33
|
-
import { hybridQuerySkills } from "./skill-qdrant.js";
|
|
34
33
|
import { generateBm25QueryEmbedding } from "./sparse-bm25.js";
|
|
35
34
|
|
|
36
35
|
/**
|
|
@@ -147,6 +146,7 @@ export async function simBatch(
|
|
|
147
146
|
text: string,
|
|
148
147
|
candidateSlugs: readonly string[],
|
|
149
148
|
config: AssistantConfig,
|
|
149
|
+
options?: { signal?: AbortSignal },
|
|
150
150
|
): Promise<Map<string, number>> {
|
|
151
151
|
if (candidateSlugs.length === 0) {
|
|
152
152
|
return new Map();
|
|
@@ -158,12 +158,16 @@ export async function simBatch(
|
|
|
158
158
|
// Sparse uses BM25: the query side encodes binary occurrences per token,
|
|
159
159
|
// and the stored doc vectors carry the IDF · TF-saturated weights — Qdrant
|
|
160
160
|
// dot product then yields the BM25 score directly.
|
|
161
|
-
|
|
161
|
+
throwIfAborted(options?.signal);
|
|
162
|
+
const denseResult = await embedWithBackend(config, [text], {
|
|
163
|
+
signal: options?.signal,
|
|
164
|
+
});
|
|
162
165
|
const denseVector = await applyCorrectionIfCalibrated(
|
|
163
166
|
denseResult.vectors[0],
|
|
164
167
|
denseResult.provider,
|
|
165
168
|
denseResult.model,
|
|
166
169
|
);
|
|
170
|
+
throwIfAborted(options?.signal);
|
|
167
171
|
const sparseVector = generateBm25QueryEmbedding(text);
|
|
168
172
|
|
|
169
173
|
const hits = await hybridQueryConceptPages(
|
|
@@ -192,92 +196,14 @@ export async function simBatch(
|
|
|
192
196
|
for (const hit of hits) {
|
|
193
197
|
scores.set(hit.slug, fuseHit(hit, maxSparse, denseWeight, sparseWeight));
|
|
194
198
|
}
|
|
199
|
+
|
|
195
200
|
return scores;
|
|
196
201
|
}
|
|
197
202
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
* dedicated `memory_v2_skills` Qdrant collection via `hybridQuerySkills`.
|
|
202
|
-
*
|
|
203
|
-
* Differences from `simBatch`:
|
|
204
|
-
* - Keys are skill `id` values (not concept-page slugs).
|
|
205
|
-
* - Restricts the query to the caller's candidate ids server-side via
|
|
206
|
-
* `hybridQuerySkills`'s `restrictToIds` parameter. Without this, when the
|
|
207
|
-
* skills collection has more skills than `ids.length`, Qdrant would
|
|
208
|
-
* return its global top-K and candidate ids absent from that top-K would
|
|
209
|
-
* silently score 0 — corrupting the activation calculation.
|
|
210
|
-
*
|
|
211
|
-
* Returns a `Map<id, score>` of fused scores in [0, 1]. Ids that did not hit
|
|
212
|
-
* either channel are absent from the map.
|
|
213
|
-
*
|
|
214
|
-
* Edge cases:
|
|
215
|
-
* - Empty `ids` → returns an empty map without touching Qdrant or the
|
|
216
|
-
* embedding backend.
|
|
217
|
-
* - Empty / whitespace-only `text` → returns an empty map without touching
|
|
218
|
-
* Qdrant or the embedding backend. Same rationale as {@link simBatch}:
|
|
219
|
-
* Gemini rejects empty content with HTTP 400, so the activation pipeline
|
|
220
|
-
* would otherwise fail on turn 1 (where the assistant-text channel is
|
|
221
|
-
* `""`). Treating the channel's contribution as 0 matches a no-hit
|
|
222
|
-
* query.
|
|
223
|
-
*/
|
|
224
|
-
export async function simSkillBatch(
|
|
225
|
-
text: string,
|
|
226
|
-
ids: readonly string[],
|
|
227
|
-
config: AssistantConfig,
|
|
228
|
-
): Promise<Map<string, number>> {
|
|
229
|
-
if (ids.length === 0) {
|
|
230
|
-
return new Map();
|
|
231
|
-
}
|
|
232
|
-
if (text.trim().length === 0) {
|
|
233
|
-
return new Map();
|
|
203
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
204
|
+
if (signal?.aborted) {
|
|
205
|
+
throw new DOMException("Aborted", "AbortError");
|
|
234
206
|
}
|
|
235
|
-
|
|
236
|
-
const denseResult = await embedWithBackend(config, [text]);
|
|
237
|
-
const denseVector = await applyCorrectionIfCalibrated(
|
|
238
|
-
denseResult.vectors[0],
|
|
239
|
-
denseResult.provider,
|
|
240
|
-
denseResult.model,
|
|
241
|
-
);
|
|
242
|
-
const sparseVector = generateBm25QueryEmbedding(text);
|
|
243
|
-
|
|
244
|
-
const hits = await hybridQuerySkills(
|
|
245
|
-
denseVector,
|
|
246
|
-
sparseVector,
|
|
247
|
-
ids.length,
|
|
248
|
-
ids,
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
if (hits.length === 0) {
|
|
252
|
-
return new Map();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Defensive post-filter — `hybridQuerySkills` restricts server-side, so
|
|
256
|
-
// every hit should already be in `ids`, but keep this guard so a buggy
|
|
257
|
-
// payload (e.g. a missing/typoed id index) can't silently inject
|
|
258
|
-
// out-of-set ids into the score map.
|
|
259
|
-
const idSet = new Set(ids);
|
|
260
|
-
const filtered = hits.filter((h) => idSet.has(h.id));
|
|
261
|
-
if (filtered.length === 0) {
|
|
262
|
-
return new Map();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const maxSparse = computeMaxSparse(filtered);
|
|
266
|
-
const { dense_weight: baseDense, sparse_weight: baseSparse } =
|
|
267
|
-
config.memory.v2;
|
|
268
|
-
const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
|
|
269
|
-
filtered,
|
|
270
|
-
maxSparse,
|
|
271
|
-
baseDense,
|
|
272
|
-
baseSparse,
|
|
273
|
-
config,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
const scores = new Map<string, number>();
|
|
277
|
-
for (const hit of filtered) {
|
|
278
|
-
scores.set(hit.id, fuseHit(hit, maxSparse, denseWeight, sparseWeight));
|
|
279
|
-
}
|
|
280
|
-
return scores;
|
|
281
207
|
}
|
|
282
208
|
|
|
283
209
|
/**
|
|
@@ -2,9 +2,10 @@ import { getConfig } from "../../config/loader.js";
|
|
|
2
2
|
import type { SkillCapabilityInput } from "../../skills/skill-memory.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Render the prose-style capability statement embedded into the
|
|
6
|
-
* `
|
|
7
|
-
* `### Skills You Can Use`. Capped at 500 chars to
|
|
5
|
+
* Render the prose-style capability statement embedded into the unified
|
|
6
|
+
* `memory_v2_concept_pages` Qdrant collection (under the `skills/<id>` slug
|
|
7
|
+
* prefix) and rendered in `### Skills You Can Use`. Capped at 500 chars to
|
|
8
|
+
* match v1's behavior.
|
|
8
9
|
*/
|
|
9
10
|
export function buildSkillContent(input: SkillCapabilityInput): string {
|
|
10
11
|
let content = `The "${input.displayName}" skill (${input.id}) is available. ${input.description}.`;
|
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
// Memory v2 — Skill catalog → embedded skill entries
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// catalog state. Including uninstalled catalog skills ensures their activation
|
|
11
|
-
// hints are discoverable by intent so the model can auto-install them.
|
|
5
|
+
// Enumerate the enabled-skill catalog AND uninstalled catalog skills, render
|
|
6
|
+
// each skill's prose statement via `buildSkillContent`, embed dense + sparse,
|
|
7
|
+
// and upsert into `memory_v2_concept_pages` under the slug `skills/<id>`.
|
|
8
|
+
// Including uninstalled catalog skills ensures their activation hints are
|
|
9
|
+
// discoverable by intent so the model can auto-install them.
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
11
|
+
// Skills share the concept-page collection rather than living in a dedicated
|
|
12
|
+
// one so the per-turn activation pipeline scores them against the same
|
|
13
|
+
// candidate ANN as concept pages, with the same decay and spread machinery.
|
|
14
|
+
// The render path branches on the `skills/` slug prefix to surface them as
|
|
15
|
+
// the `### Skills You Can Use` subsection.
|
|
16
|
+
//
|
|
17
|
+
// Skill entries are kept in a small in-process cache so the render path can
|
|
18
|
+
// fetch a `SkillEntry` synchronously by id without round-tripping to Qdrant.
|
|
19
|
+
// The cache is replaced atomically at the end of a successful seed run; on
|
|
20
|
+
// error the prior cache stays intact (skills are best-effort).
|
|
17
21
|
|
|
18
22
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
19
23
|
import { getConfig } from "../../config/loader.js";
|
|
@@ -30,15 +34,31 @@ import {
|
|
|
30
34
|
embedWithBackend,
|
|
31
35
|
generateSparseEmbedding,
|
|
32
36
|
} from "../embedding-backend.js";
|
|
37
|
+
import {
|
|
38
|
+
pruneSlugsWithPrefixExcept,
|
|
39
|
+
upsertConceptPageEmbedding,
|
|
40
|
+
} from "./qdrant.js";
|
|
33
41
|
import {
|
|
34
42
|
augmentMcpSetupDescription,
|
|
35
43
|
buildSkillContent,
|
|
36
44
|
} from "./skill-content.js";
|
|
37
|
-
import { pruneSkillsExcept, upsertSkillEmbedding } from "./skill-qdrant.js";
|
|
38
45
|
import type { SkillEntry } from "./types.js";
|
|
39
46
|
|
|
40
47
|
const log = getLogger("memory-v2-skill-store");
|
|
41
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Slug prefix under which skill embeddings are indexed in
|
|
51
|
+
* `memory_v2_concept_pages`. Concept-page slugs must match
|
|
52
|
+
* `[a-z0-9][a-z0-9-]*(/...)*`, and `skills` matches that pattern, so the
|
|
53
|
+
* prefix coexists with hand-authored concept pages without escape work.
|
|
54
|
+
*/
|
|
55
|
+
export const SKILL_SLUG_PREFIX = "skills/";
|
|
56
|
+
|
|
57
|
+
/** Compose the unified-collection slug for a skill id. */
|
|
58
|
+
export function skillSlugFor(id: string): string {
|
|
59
|
+
return `${SKILL_SLUG_PREFIX}${id}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
/**
|
|
43
63
|
* Module-level cache of rendered skill entries keyed by skill id. `null` until
|
|
44
64
|
* the first successful seed run completes; replaced atomically on each
|
|
@@ -47,30 +67,27 @@ const log = getLogger("memory-v2-skill-store");
|
|
|
47
67
|
let entries: Map<string, SkillEntry> | null = null;
|
|
48
68
|
|
|
49
69
|
/**
|
|
50
|
-
* Seed (or re-seed)
|
|
51
|
-
*
|
|
52
|
-
*
|
|
70
|
+
* Seed (or re-seed) skill embeddings into the unified concept-page collection.
|
|
71
|
+
* Idempotent: safe to call repeatedly. Best-effort: never throws — any
|
|
72
|
+
* failure leaves the prior `entries` cache in place and logs a warning.
|
|
53
73
|
*
|
|
54
74
|
* Steps:
|
|
55
|
-
* 1. Enumerate the local skill catalog and resolve each skill's enabled
|
|
56
|
-
* (`resolveSkillStates`).
|
|
57
|
-
* 2. Build a `
|
|
58
|
-
*
|
|
59
|
-
*
|
|
75
|
+
* 1. Enumerate the local skill catalog and resolve each skill's enabled
|
|
76
|
+
* state (`resolveSkillStates`).
|
|
77
|
+
* 2. Build a `SkillEntry` per enabled skill, applying the mcp-setup
|
|
78
|
+
* augmentation and the prose-style content render (`buildSkillContent`,
|
|
79
|
+
* capped at 500 chars).
|
|
60
80
|
* 3. Defense-in-depth feature-flag filter: drop any skill whose declared
|
|
61
|
-
* `metadata.vellum.feature-flag` is currently disabled.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* 3b. Fetch the full remote catalog and seed any uninstalled skills so their
|
|
66
|
-
* activation hints are discoverable by semantic search. Best-effort: if
|
|
67
|
-
* the catalog fetch fails, only installed skills are seeded.
|
|
81
|
+
* `metadata.vellum.feature-flag` is currently disabled.
|
|
82
|
+
* 3b. Fetch the full remote catalog and seed any uninstalled skills so
|
|
83
|
+
* their activation hints are discoverable by semantic search. Best-effort:
|
|
84
|
+
* if the catalog fetch fails, only installed skills are seeded.
|
|
68
85
|
* 4. Embed all `content` strings in a single dense `embedWithBackend` call,
|
|
69
86
|
* and a per-skill synchronous `generateSparseEmbedding`.
|
|
70
|
-
* 5. Upsert one Qdrant point per skill via `
|
|
71
|
-
* deterministically on id
|
|
72
|
-
* 6. Call `
|
|
73
|
-
* points from prior catalog state (e.g. uninstalled skills).
|
|
87
|
+
* 5. Upsert one Qdrant point per skill via `upsertConceptPageEmbedding`
|
|
88
|
+
* keyed deterministically on slug `skills/<id>`.
|
|
89
|
+
* 6. Call `pruneSlugsWithPrefixExcept(SKILL_SLUG_PREFIX, ...)` to drop any
|
|
90
|
+
* stale points from prior catalog state (e.g. uninstalled skills).
|
|
74
91
|
* 7. Replace the module-level `entries` cache with the freshly built map.
|
|
75
92
|
*/
|
|
76
93
|
export async function seedV2SkillEntries(): Promise<void> {
|
|
@@ -83,8 +100,7 @@ export async function seedV2SkillEntries(): Promise<void> {
|
|
|
83
100
|
// Track every locally-installed skill id (regardless of enabled/disabled
|
|
84
101
|
// state) so the catalog-seeding loop below treats them all as "installed"
|
|
85
102
|
// and never re-seeds a disabled skill from `getCatalog()` as if it were
|
|
86
|
-
// uninstalled.
|
|
87
|
-
// keys off `loadSkillCatalog()` (the installed set) for the same reason.
|
|
103
|
+
// uninstalled.
|
|
88
104
|
const installedIds = new Set<string>(catalog.map((s) => s.id));
|
|
89
105
|
|
|
90
106
|
// Build the input list, applying the mcp-setup description augmentation
|
|
@@ -100,8 +116,8 @@ export async function seedV2SkillEntries(): Promise<void> {
|
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
// Seed uninstalled catalog skills so their activation hints are
|
|
103
|
-
// discoverable by intent
|
|
104
|
-
//
|
|
119
|
+
// discoverable by intent. Track whether the catalog was available so we
|
|
120
|
+
// can guard pruning below.
|
|
105
121
|
let catalogAvailable = false;
|
|
106
122
|
try {
|
|
107
123
|
const fullCatalog = await getCatalog();
|
|
@@ -135,24 +151,29 @@ export async function seedV2SkillEntries(): Promise<void> {
|
|
|
135
151
|
|
|
136
152
|
const now = Date.now();
|
|
137
153
|
const nextEntries = new Map<string, SkillEntry>();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
await Promise.all(
|
|
155
|
+
seeds.map((seed, i) =>
|
|
156
|
+
upsertConceptPageEmbedding({
|
|
157
|
+
slug: skillSlugFor(seed.id),
|
|
158
|
+
dense: denseVectors[i],
|
|
159
|
+
sparse: generateSparseEmbedding(seed.content),
|
|
160
|
+
updatedAt: now,
|
|
161
|
+
}),
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
for (const seed of seeds) {
|
|
146
165
|
nextEntries.set(seed.id, seed);
|
|
147
166
|
}
|
|
148
167
|
|
|
149
|
-
// Prune stale
|
|
150
|
-
// network failure or cold cache), we cannot enumerate which
|
|
151
|
-
// catalog skills should exist, so skip pruning entirely to
|
|
152
|
-
// aggressively removing previously-seeded catalog skill embeddings.
|
|
153
|
-
// Mirrors v1's safeguard in capability-seed.ts (lines 124–143).
|
|
168
|
+
// Prune stale skill slugs. When the catalog is unavailable (empty array
|
|
169
|
+
// from network failure or cold cache), we cannot enumerate which
|
|
170
|
+
// uninstalled catalog skills should exist, so skip pruning entirely to
|
|
171
|
+
// avoid aggressively removing previously-seeded catalog skill embeddings.
|
|
154
172
|
if (catalogAvailable) {
|
|
155
|
-
await
|
|
173
|
+
await pruneSlugsWithPrefixExcept(
|
|
174
|
+
SKILL_SLUG_PREFIX,
|
|
175
|
+
seeds.map((s) => s.id),
|
|
176
|
+
);
|
|
156
177
|
} else {
|
|
157
178
|
log.info(
|
|
158
179
|
"Catalog unavailable — skipping skill pruning to preserve prior catalog embeddings",
|
|
@@ -169,20 +190,22 @@ export async function seedV2SkillEntries(): Promise<void> {
|
|
|
169
190
|
/**
|
|
170
191
|
* Synchronous lookup of a previously-seeded `SkillEntry` by skill id. Returns
|
|
171
192
|
* `null` when the cache has not yet been populated, when the id is unknown,
|
|
172
|
-
* or when a prior seed run dropped the id (e.g. the skill was disabled).
|
|
173
|
-
*
|
|
193
|
+
* or when a prior seed run dropped the id (e.g. the skill was disabled).
|
|
194
|
+
*
|
|
195
|
+
* Accepts either a bare skill id (`example-skill`) or its unified-collection
|
|
196
|
+
* slug (`skills/example-skill`) so render-side callers can pass through what
|
|
197
|
+
* they have without a manual prefix strip.
|
|
174
198
|
*/
|
|
175
|
-
export function getSkillCapability(
|
|
199
|
+
export function getSkillCapability(idOrSlug: string): SkillEntry | null {
|
|
200
|
+
const id = idOrSlug.startsWith(SKILL_SLUG_PREFIX)
|
|
201
|
+
? idOrSlug.slice(SKILL_SLUG_PREFIX.length)
|
|
202
|
+
: idOrSlug;
|
|
176
203
|
return entries?.get(id) ?? null;
|
|
177
204
|
}
|
|
178
205
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
* run completes.
|
|
183
|
-
*/
|
|
184
|
-
export function getAllSkillIds(): string[] {
|
|
185
|
-
return entries ? [...entries.keys()] : [];
|
|
206
|
+
/** True iff the slug refers to a skill entry in the unified collection. */
|
|
207
|
+
export function isSkillSlug(slug: string): boolean {
|
|
208
|
+
return slug.startsWith(SKILL_SLUG_PREFIX);
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
/** @internal Test-only: clear the module-level cache. */
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// content through when `mode === "full"` (first turn / post-compaction),
|
|
18
18
|
// matching the existing PKB auto-inject pattern.
|
|
19
19
|
|
|
20
|
+
import type { ChannelId } from "../../channels/types.js";
|
|
20
21
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
21
22
|
import { loadConfig } from "../../config/loader.js";
|
|
22
23
|
import { readPromptFile } from "../../prompts/system-prompt.js";
|
|
@@ -61,3 +62,24 @@ export function readMemoryV2StaticContent(): string | null {
|
|
|
61
62
|
}
|
|
62
63
|
return sections.length > 0 ? sections.join("\n\n") : null;
|
|
63
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Static memory holds the user's aggregate personal pages
|
|
68
|
+
* (essentials/threads/recent/buffer). Block injection when a non-guardian
|
|
69
|
+
* actor reaches the assistant over a remote channel — otherwise the model
|
|
70
|
+
* can be prompt-injected into reciting private memory. Internal flows
|
|
71
|
+
* (`sourceChannel: "vellum"`) and turns with no trust context pass through
|
|
72
|
+
* unchanged; this gate exists only to keep remote untrusted actors out.
|
|
73
|
+
*/
|
|
74
|
+
export function shouldLoadMemoryV2Static(args: {
|
|
75
|
+
shouldInjectNowAndPkb: boolean;
|
|
76
|
+
sourceChannel: ChannelId | undefined;
|
|
77
|
+
isTrustedActor: boolean;
|
|
78
|
+
}): boolean {
|
|
79
|
+
if (!args.shouldInjectNowAndPkb) return false;
|
|
80
|
+
const isRemoteUntrustedActor =
|
|
81
|
+
args.sourceChannel !== undefined &&
|
|
82
|
+
args.sourceChannel !== "vellum" &&
|
|
83
|
+
!args.isTrustedActor;
|
|
84
|
+
return !isRemoteUntrustedActor;
|
|
85
|
+
}
|
package/src/memory/v2/types.ts
CHANGED
|
@@ -85,20 +85,20 @@ export const ActivationStateSchema = z.object({
|
|
|
85
85
|
export type ActivationState = z.infer<typeof ActivationStateSchema>;
|
|
86
86
|
|
|
87
87
|
// ---------------------------------------------------------------------------
|
|
88
|
-
// Skill
|
|
88
|
+
// Skill entries (synthetic concept-collection rows, not on-disk pages)
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Per-skill capability snapshot held in-process and embedded into the
|
|
93
|
-
* `
|
|
94
|
-
* `buildSkillContent` string — already capped at
|
|
95
|
-
* already containing the skill's display name — and
|
|
96
|
-
* what we render verbatim in `### Skills You Can Use`.
|
|
92
|
+
* Per-skill capability snapshot held in-process and embedded into the unified
|
|
93
|
+
* `memory_v2_concept_pages` Qdrant collection under the slug `skills/<id>`.
|
|
94
|
+
* `content` is the rendered `buildSkillContent` string — already capped at
|
|
95
|
+
* 500 chars upstream and already containing the skill's display name — and
|
|
96
|
+
* is what we embed and what we render verbatim in `### Skills You Can Use`.
|
|
97
97
|
*
|
|
98
|
-
* Plain interface (no Zod) because skill data does not cross a
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
98
|
+
* Plain interface (no Zod) because skill data does not cross a serialization
|
|
99
|
+
* boundary: it is built in-process by `seedV2SkillEntries` and read in-process
|
|
100
|
+
* by `renderInjectionBlock`. The Qdrant payload is not parsed back through
|
|
101
|
+
* this type.
|
|
102
102
|
*/
|
|
103
103
|
export interface SkillEntry {
|
|
104
104
|
id: string;
|
|
@@ -505,6 +505,19 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
|
505
505
|
body: str(payload.body, "A watcher event requires your attention"),
|
|
506
506
|
}),
|
|
507
507
|
|
|
508
|
+
"heartbeat.alert": (payload) => {
|
|
509
|
+
const body = str(
|
|
510
|
+
payload.summary,
|
|
511
|
+
str(payload.body, "Your assistant found something worth your attention."),
|
|
512
|
+
);
|
|
513
|
+
return {
|
|
514
|
+
title: str(payload.title, "Heartbeat Alert"),
|
|
515
|
+
body,
|
|
516
|
+
conversationTitle: str(payload.conversationTitle, "Heartbeat"),
|
|
517
|
+
conversationSeedMessage: body,
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
|
|
508
521
|
"tool_confirmation.required_action": (payload) => ({
|
|
509
522
|
title: "Tool Confirmation",
|
|
510
523
|
body: str(payload.toolName, "A tool") + " requires your confirmation",
|
|
@@ -101,6 +101,10 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
|
|
|
101
101
|
description:
|
|
102
102
|
"OAuth credential health issue detected (expired, revoked, missing scopes)",
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
id: "heartbeat.alert",
|
|
106
|
+
description: "Heartbeat found something worth surfacing to the guardian",
|
|
107
|
+
},
|
|
104
108
|
] as const;
|
|
105
109
|
|
|
106
110
|
export type NotificationSourceEventName =
|
package/src/oauth/AGENTS.md
CHANGED
|
@@ -54,7 +54,9 @@ Most existing logos come from [Simple Icons](https://simpleicons.org) (CC0-licen
|
|
|
54
54
|
|
|
55
55
|
If the service is not on Simple Icons, source or create an SVG and convert it the same way. The result must be a true vector PDF (not a rasterized image wrapped in PDF) so it scales cleanly.
|
|
56
56
|
|
|
57
|
-
The `logoUrl` field in `seed-providers.ts`
|
|
57
|
+
The `logoUrl` field in `seed-providers.ts` serves as the remote fallback (most providers use a Simple Icons CDN URL like `https://cdn.simpleicons.org/acme`). The client renders the local PDF first, then falls back to `logoUrl`, then to an initials avatar.
|
|
58
|
+
|
|
59
|
+
For brands Simple Icons doesn't host (e.g. Salesforce, which Simple Icons removed for trademark reasons), use the same `glincker/thesvg` source via jsDelivr — `https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/<key>/default.svg`. The recognised `logoUrl` prefixes are enforced by `oauth-provider-seed-logos.test.ts`; if you need a third source, extend that allowlist alongside the manifest in `clients/shared/Resources/integration-logos-manifest.json`.
|
|
58
60
|
|
|
59
61
|
### 5. Secret patterns (if applicable) — `../security/secret-patterns.ts`
|
|
60
62
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
_clearAllOAuthConnectStates,
|
|
5
|
+
clearExpiredOAuthConnectStates,
|
|
6
|
+
getOAuthConnectState,
|
|
7
|
+
setOAuthConnectComplete,
|
|
8
|
+
setOAuthConnectError,
|
|
9
|
+
setOAuthConnectPending,
|
|
10
|
+
} from "../oauth-connect-state.js";
|
|
11
|
+
|
|
12
|
+
describe("oauth-connect-state", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
_clearAllOAuthConnectStates();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("setOAuthConnectPending → getOAuthConnectState returns pending", () => {
|
|
18
|
+
setOAuthConnectPending("state-1", "google");
|
|
19
|
+
const result = getOAuthConnectState("state-1");
|
|
20
|
+
expect(result).toMatchObject({ status: "pending", service: "google" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("setOAuthConnectComplete without accountInfo → returns complete", () => {
|
|
24
|
+
setOAuthConnectComplete("state-1", "google");
|
|
25
|
+
const result = getOAuthConnectState("state-1");
|
|
26
|
+
expect(result).toMatchObject({ status: "complete", service: "google" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("setOAuthConnectComplete with accountInfo → returns complete with accountInfo", () => {
|
|
30
|
+
setOAuthConnectComplete("state-1", "google", "user@example.com");
|
|
31
|
+
const result = getOAuthConnectState("state-1");
|
|
32
|
+
expect(result).toMatchObject({
|
|
33
|
+
status: "complete",
|
|
34
|
+
service: "google",
|
|
35
|
+
accountInfo: "user@example.com",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("setOAuthConnectComplete with grantedScopes → returns complete with grantedScopes", () => {
|
|
40
|
+
setOAuthConnectComplete("state-1", "google", "user@example.com", ["scope:read", "scope:write"]);
|
|
41
|
+
const result = getOAuthConnectState("state-1");
|
|
42
|
+
expect(result).toMatchObject({
|
|
43
|
+
status: "complete",
|
|
44
|
+
service: "google",
|
|
45
|
+
accountInfo: "user@example.com",
|
|
46
|
+
grantedScopes: ["scope:read", "scope:write"],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("setOAuthConnectError → returns error with message", () => {
|
|
51
|
+
setOAuthConnectError("state-1", "google", "token exchange failed");
|
|
52
|
+
const result = getOAuthConnectState("state-1");
|
|
53
|
+
expect(result).toMatchObject({
|
|
54
|
+
status: "error",
|
|
55
|
+
service: "google",
|
|
56
|
+
error: "token exchange failed",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("re-setting same state token overwrites previous", () => {
|
|
61
|
+
setOAuthConnectPending("state-1", "google");
|
|
62
|
+
setOAuthConnectComplete("state-1", "google", "user@example.com");
|
|
63
|
+
const result = getOAuthConnectState("state-1");
|
|
64
|
+
expect(result?.status).toBe("complete");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("getOAuthConnectState returns null for unknown state", () => {
|
|
68
|
+
expect(getOAuthConnectState("nonexistent")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("_clearAllOAuthConnectStates removes all entries", () => {
|
|
72
|
+
setOAuthConnectPending("state-1", "google");
|
|
73
|
+
setOAuthConnectPending("state-2", "github");
|
|
74
|
+
_clearAllOAuthConnectStates();
|
|
75
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
76
|
+
expect(getOAuthConnectState("state-2")).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("clearExpiredOAuthConnectStates removes expired pending entries", () => {
|
|
80
|
+
setOAuthConnectPending("state-1", "google");
|
|
81
|
+
// Advance Date.now by 6 minutes past PENDING_TTL_MS (5 min)
|
|
82
|
+
const originalNow = Date.now;
|
|
83
|
+
Date.now = () => originalNow() + 6 * 60 * 1000;
|
|
84
|
+
clearExpiredOAuthConnectStates();
|
|
85
|
+
Date.now = originalNow;
|
|
86
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("clearExpiredOAuthConnectStates removes expired complete entries (past 60s grace)", () => {
|
|
90
|
+
setOAuthConnectComplete("state-1", "google");
|
|
91
|
+
const originalNow = Date.now;
|
|
92
|
+
Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
|
|
93
|
+
clearExpiredOAuthConnectStates();
|
|
94
|
+
Date.now = originalNow;
|
|
95
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("clearExpiredOAuthConnectStates removes expired error entries (past 60s grace)", () => {
|
|
99
|
+
setOAuthConnectError("state-1", "google", "token exchange failed");
|
|
100
|
+
const originalNow = Date.now;
|
|
101
|
+
Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
|
|
102
|
+
clearExpiredOAuthConnectStates();
|
|
103
|
+
Date.now = originalNow;
|
|
104
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("clearExpiredOAuthConnectStates does not remove non-expired pending entries", () => {
|
|
108
|
+
setOAuthConnectPending("state-1", "google");
|
|
109
|
+
clearExpiredOAuthConnectStates(); // called without advancing time
|
|
110
|
+
expect(getOAuthConnectState("state-1")).not.toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("sweep-on-insert: setOAuthConnectPending purges expired entries before inserting new one", () => {
|
|
114
|
+
// 1. Add an entry that will expire
|
|
115
|
+
setOAuthConnectPending("expired-state", "google");
|
|
116
|
+
|
|
117
|
+
// 2. Advance Date.now past the PENDING_TTL_MS (5 min)
|
|
118
|
+
const originalNow = Date.now;
|
|
119
|
+
Date.now = () => originalNow() + 6 * 60 * 1000;
|
|
120
|
+
|
|
121
|
+
// 3. Insert a new entry — this should trigger clearExpiredOAuthConnectStates() internally
|
|
122
|
+
setOAuthConnectPending("new-state", "github");
|
|
123
|
+
|
|
124
|
+
// 4. Restore Date.now before assertions (getOAuthConnectState also calls clearExpiredOAuthConnectStates)
|
|
125
|
+
Date.now = originalNow;
|
|
126
|
+
|
|
127
|
+
// The expired entry must have been swept out during the insert
|
|
128
|
+
// Use the map directly via getOAuthConnectState — expired-state is gone
|
|
129
|
+
// We call _clearAllOAuthConnectStates in beforeEach so we know the map started empty.
|
|
130
|
+
// After the insert the map should only contain "new-state".
|
|
131
|
+
const expiredResult = getOAuthConnectState("expired-state");
|
|
132
|
+
expect(expiredResult).toBeNull();
|
|
133
|
+
|
|
134
|
+
const newResult = getOAuthConnectState("new-state");
|
|
135
|
+
expect(newResult).toMatchObject({ status: "pending", service: "github" });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -81,6 +81,7 @@ export interface OAuthConnectOptions {
|
|
|
81
81
|
success: boolean;
|
|
82
82
|
service: string;
|
|
83
83
|
accountInfo?: string;
|
|
84
|
+
grantedScopes?: string[];
|
|
84
85
|
error?: string;
|
|
85
86
|
}) => void;
|
|
86
87
|
}
|
|
@@ -256,6 +257,7 @@ export async function orchestrateOAuthConnect(
|
|
|
256
257
|
success: true,
|
|
257
258
|
service: options.service,
|
|
258
259
|
accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
|
|
260
|
+
grantedScopes: result.grantedScopes,
|
|
259
261
|
});
|
|
260
262
|
} catch (err) {
|
|
261
263
|
log.error(
|