@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
|
@@ -68,17 +68,6 @@ const state = {
|
|
|
68
68
|
points: Array<{ score?: number; payload: Record<string, unknown> }>;
|
|
69
69
|
}>,
|
|
70
70
|
},
|
|
71
|
-
// Separate response queue for the dedicated `memory_v2_skills` collection
|
|
72
|
-
// so a test asserting on skill activation does not have to interleave
|
|
73
|
-
// responses with concept-page queries.
|
|
74
|
-
skillQueryResponses: {
|
|
75
|
-
dense: [] as Array<{
|
|
76
|
-
points: Array<{ score?: number; payload: Record<string, unknown> }>;
|
|
77
|
-
}>,
|
|
78
|
-
sparse: [] as Array<{
|
|
79
|
-
points: Array<{ score?: number; payload: Record<string, unknown> }>;
|
|
80
|
-
}>,
|
|
81
|
-
},
|
|
82
71
|
queryCalls: [] as Array<{
|
|
83
72
|
collection: string;
|
|
84
73
|
using: string;
|
|
@@ -126,11 +115,7 @@ class MockQdrantClient {
|
|
|
126
115
|
filter: params.filter,
|
|
127
116
|
});
|
|
128
117
|
const channel = params.using as "dense" | "sparse";
|
|
129
|
-
|
|
130
|
-
name === "memory_v2_skills"
|
|
131
|
-
? state.skillQueryResponses[channel]
|
|
132
|
-
: state.queryResponses[channel];
|
|
133
|
-
return queue.shift() ?? { points: [] };
|
|
118
|
+
return state.queryResponses[channel].shift() ?? { points: [] };
|
|
134
119
|
}
|
|
135
120
|
}
|
|
136
121
|
|
|
@@ -138,6 +123,37 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
138
123
|
QdrantClient: MockQdrantClient,
|
|
139
124
|
}));
|
|
140
125
|
|
|
126
|
+
// Reranker mock — keeps the activation tests hermetic when rerank.enabled is
|
|
127
|
+
// flipped on by an integration case. Tests stage `rerankState.scores` to
|
|
128
|
+
// program the boost outcome. The activation pipeline now passes both the
|
|
129
|
+
// user-channel and assistant-channel queries into a single rerank call, so
|
|
130
|
+
// `rerankState.calls` records the full `queries` array per invocation.
|
|
131
|
+
const rerankState = {
|
|
132
|
+
scores: null as Map<string, number> | null,
|
|
133
|
+
calls: [] as Array<{ queries: string[]; candidates: string[] }>,
|
|
134
|
+
};
|
|
135
|
+
mock.module("../reranker.js", () => ({
|
|
136
|
+
rerankCandidates: async (
|
|
137
|
+
queries: readonly string[],
|
|
138
|
+
candidates: readonly string[],
|
|
139
|
+
): Promise<Array<Map<string, number>>> => {
|
|
140
|
+
rerankState.calls.push({
|
|
141
|
+
queries: [...queries],
|
|
142
|
+
candidates: [...candidates],
|
|
143
|
+
});
|
|
144
|
+
return queries.map(() => {
|
|
145
|
+
if (rerankState.scores === null) return new Map();
|
|
146
|
+
const out = new Map<string, number>();
|
|
147
|
+
for (const slug of candidates) {
|
|
148
|
+
const v = rerankState.scores.get(slug);
|
|
149
|
+
if (v !== undefined) out.set(slug, v);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
_resetRerankCacheForTests: () => {},
|
|
155
|
+
}));
|
|
156
|
+
|
|
141
157
|
// Static `import type` is fine — types erase, so they don't run module-init
|
|
142
158
|
// code that would race the mocks above.
|
|
143
159
|
import type { EdgeIndex } from "../edge-index.js";
|
|
@@ -145,15 +161,11 @@ import type { ActivationState } from "../types.js";
|
|
|
145
161
|
|
|
146
162
|
const {
|
|
147
163
|
computeOwnActivation,
|
|
148
|
-
computeSkillActivation,
|
|
149
164
|
selectCandidates,
|
|
150
165
|
selectInjections,
|
|
151
|
-
selectSkillInjections,
|
|
152
166
|
spreadActivation,
|
|
153
167
|
} = await import("../activation.js");
|
|
154
168
|
const { _resetMemoryV2QdrantForTests } = await import("../qdrant.js");
|
|
155
|
-
const { _resetMemoryV2SkillQdrantForTests } =
|
|
156
|
-
await import("../skill-qdrant.js");
|
|
157
169
|
|
|
158
170
|
// ---------------------------------------------------------------------------
|
|
159
171
|
// Helpers
|
|
@@ -166,16 +178,15 @@ function resetState(): void {
|
|
|
166
178
|
state.sparseReturn = { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] };
|
|
167
179
|
state.queryResponses.dense.length = 0;
|
|
168
180
|
state.queryResponses.sparse.length = 0;
|
|
169
|
-
state.skillQueryResponses.dense.length = 0;
|
|
170
|
-
state.skillQueryResponses.sparse.length = 0;
|
|
171
181
|
state.queryCalls.length = 0;
|
|
182
|
+
rerankState.scores = null;
|
|
183
|
+
rerankState.calls.length = 0;
|
|
172
184
|
// Bun's `mock.module` persists across files in the same process, so the
|
|
173
|
-
// qdrant
|
|
174
|
-
// instance from a sibling test file (e.g. sim.test.ts). Resetting
|
|
185
|
+
// qdrant module's `_client` singleton may already hold a MockQdrantClient
|
|
186
|
+
// instance from a sibling test file (e.g. sim.test.ts). Resetting the
|
|
175
187
|
// cache AND any latched readiness forces a fresh `new QdrantClient()` —
|
|
176
188
|
// which under our mock above resolves to *this* file's MockQdrantClient.
|
|
177
189
|
_resetMemoryV2QdrantForTests();
|
|
178
|
-
_resetMemoryV2SkillQdrantForTests();
|
|
179
190
|
}
|
|
180
191
|
|
|
181
192
|
/**
|
|
@@ -554,6 +565,257 @@ describe("computeOwnActivation", () => {
|
|
|
554
565
|
// No prior state → prev=0 → priorContribution=0 regardless of `d`.
|
|
555
566
|
expect(out.breakdown.get("fresh")?.priorContribution).toBe(0);
|
|
556
567
|
});
|
|
568
|
+
|
|
569
|
+
test("rerank boost on user/assistant flips top-1 when fused had it second", async () => {
|
|
570
|
+
// Three Qdrant queries fire in parallel inside computeOwnActivation:
|
|
571
|
+
// user, assistant, now. Stage identical hits for each so the only signal
|
|
572
|
+
// separating slugs is the rerank boost on the user + assistant channels.
|
|
573
|
+
const stagedHits = [
|
|
574
|
+
{ slug: "lexical", denseScore: 0.6, sparseScore: 0 },
|
|
575
|
+
{ slug: "semantic", denseScore: 0.5, sparseScore: 0 },
|
|
576
|
+
];
|
|
577
|
+
stageHybridResponse(stagedHits); // user channel
|
|
578
|
+
stageHybridResponse(stagedHits); // assistant channel
|
|
579
|
+
stageHybridResponse(stagedHits); // now channel
|
|
580
|
+
rerankState.scores = new Map([
|
|
581
|
+
["lexical", 0.05],
|
|
582
|
+
["semantic", 0.95],
|
|
583
|
+
]);
|
|
584
|
+
|
|
585
|
+
const config = {
|
|
586
|
+
memory: {
|
|
587
|
+
v2: {
|
|
588
|
+
d: 0.0,
|
|
589
|
+
c_user: 0.5,
|
|
590
|
+
c_assistant: 0.5,
|
|
591
|
+
c_now: 0.0,
|
|
592
|
+
dense_weight: 1.0,
|
|
593
|
+
sparse_weight: 0.0,
|
|
594
|
+
rerank: {
|
|
595
|
+
enabled: true,
|
|
596
|
+
top_k: 50,
|
|
597
|
+
alpha: 0.5,
|
|
598
|
+
model: "test-model",
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
} as unknown as AssistantConfig;
|
|
603
|
+
|
|
604
|
+
const out = await computeOwnActivation({
|
|
605
|
+
candidates: new Set(["lexical", "semantic"]),
|
|
606
|
+
priorState: null,
|
|
607
|
+
userText: "u",
|
|
608
|
+
assistantText: "a",
|
|
609
|
+
nowText: "n",
|
|
610
|
+
config,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Without rerank: lexical (0.6) would beat semantic (0.5) on both
|
|
614
|
+
// user and assistant channels.
|
|
615
|
+
// With rerank (alpha=0.5):
|
|
616
|
+
// lexical: 0.6 + 0.5 · (0.05/0.95) ≈ 0.626
|
|
617
|
+
// semantic: 0.5 + 0.5 · 1.0 = 1.0
|
|
618
|
+
// The semantic candidate now wins on both rerank-boosted channels.
|
|
619
|
+
expect(out.activation.get("semantic")!).toBeGreaterThan(
|
|
620
|
+
out.activation.get("lexical")!,
|
|
621
|
+
);
|
|
622
|
+
// Both rerank-enabled channels ride in a single batched rerank call.
|
|
623
|
+
expect(rerankState.calls).toHaveLength(1);
|
|
624
|
+
expect(rerankState.calls[0].queries).toEqual(["u", "a"]);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("rerank pool is the unified top-K by pre-rerank A_o, not per-channel fused", async () => {
|
|
628
|
+
// Three candidates. The per-channel fused-sim top-2s would have picked
|
|
629
|
+
// different sets:
|
|
630
|
+
// user channel: a=0.9, b=0.5, c=0.4 → per-channel top-2 = [a, b]
|
|
631
|
+
// assistant channel: a=0.5, b=0.4, c=0.9 → per-channel top-2 = [c, a]
|
|
632
|
+
// But pre-rerank A_o (c_user=c_assistant=0.5) is:
|
|
633
|
+
// a = 0.5·0.9 + 0.5·0.5 = 0.70
|
|
634
|
+
// b = 0.5·0.5 + 0.5·0.4 = 0.45
|
|
635
|
+
// c = 0.5·0.4 + 0.5·0.9 = 0.65
|
|
636
|
+
// → unified top-2 = [a, c]. b drops out, even though it would have made
|
|
637
|
+
// the user-channel pool under the old per-channel selection.
|
|
638
|
+
stageHybridResponse([
|
|
639
|
+
{ slug: "a", denseScore: 0.9 },
|
|
640
|
+
{ slug: "b", denseScore: 0.5 },
|
|
641
|
+
{ slug: "c", denseScore: 0.4 },
|
|
642
|
+
]); // user
|
|
643
|
+
stageHybridResponse([
|
|
644
|
+
{ slug: "a", denseScore: 0.5 },
|
|
645
|
+
{ slug: "b", denseScore: 0.4 },
|
|
646
|
+
{ slug: "c", denseScore: 0.9 },
|
|
647
|
+
]); // assistant
|
|
648
|
+
stageHybridResponse([]); // now (no signal)
|
|
649
|
+
rerankState.scores = new Map([
|
|
650
|
+
["a", 0.5],
|
|
651
|
+
["b", 0.5],
|
|
652
|
+
["c", 0.5],
|
|
653
|
+
]);
|
|
654
|
+
|
|
655
|
+
const config = {
|
|
656
|
+
memory: {
|
|
657
|
+
v2: {
|
|
658
|
+
d: 0.0,
|
|
659
|
+
c_user: 0.5,
|
|
660
|
+
c_assistant: 0.5,
|
|
661
|
+
c_now: 0.0,
|
|
662
|
+
dense_weight: 1.0,
|
|
663
|
+
sparse_weight: 0.0,
|
|
664
|
+
rerank: {
|
|
665
|
+
enabled: true,
|
|
666
|
+
top_k: 2,
|
|
667
|
+
alpha: 0.3,
|
|
668
|
+
model: "test-model",
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
} as unknown as AssistantConfig;
|
|
673
|
+
|
|
674
|
+
await computeOwnActivation({
|
|
675
|
+
candidates: new Set(["a", "b", "c"]),
|
|
676
|
+
priorState: null,
|
|
677
|
+
userText: "u",
|
|
678
|
+
assistantText: "a",
|
|
679
|
+
nowText: "",
|
|
680
|
+
config,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Single batched rerank call carrying both channel queries against the
|
|
684
|
+
// unified slug set, sorted by pre-rerank A_o descending.
|
|
685
|
+
expect(rerankState.calls).toHaveLength(1);
|
|
686
|
+
expect(rerankState.calls[0].queries).toEqual(["u", "a"]);
|
|
687
|
+
expect(rerankState.calls[0].candidates).toEqual(["a", "c"]);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("rerank-disabled candidates outside the unified pool get zero boost", async () => {
|
|
691
|
+
// Two candidates, top_k=1. The lower pre-rerank A_o slug must end up
|
|
692
|
+
// with simUserRerankBoost=0 / simAssistantRerankBoost=0 in the breakdown.
|
|
693
|
+
stageHybridResponse([
|
|
694
|
+
{ slug: "winner", denseScore: 0.9 },
|
|
695
|
+
{ slug: "loser", denseScore: 0.2 },
|
|
696
|
+
]); // user
|
|
697
|
+
stageHybridResponse([
|
|
698
|
+
{ slug: "winner", denseScore: 0.9 },
|
|
699
|
+
{ slug: "loser", denseScore: 0.2 },
|
|
700
|
+
]); // assistant
|
|
701
|
+
stageHybridResponse([]); // now
|
|
702
|
+
// The mocked reranker hands back scores for whatever slugs it's
|
|
703
|
+
// called with. Stage scores for both; the assertion below is that
|
|
704
|
+
// the loser still receives 0 because it's never sent to the
|
|
705
|
+
// reranker — top_k=1 cuts it off.
|
|
706
|
+
rerankState.scores = new Map([
|
|
707
|
+
["winner", 0.5],
|
|
708
|
+
["loser", 0.5],
|
|
709
|
+
]);
|
|
710
|
+
|
|
711
|
+
const config = {
|
|
712
|
+
memory: {
|
|
713
|
+
v2: {
|
|
714
|
+
d: 0.0,
|
|
715
|
+
c_user: 0.5,
|
|
716
|
+
c_assistant: 0.5,
|
|
717
|
+
c_now: 0.0,
|
|
718
|
+
dense_weight: 1.0,
|
|
719
|
+
sparse_weight: 0.0,
|
|
720
|
+
rerank: {
|
|
721
|
+
enabled: true,
|
|
722
|
+
top_k: 1,
|
|
723
|
+
alpha: 0.3,
|
|
724
|
+
model: "test-model",
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
} as unknown as AssistantConfig;
|
|
729
|
+
|
|
730
|
+
const out = await computeOwnActivation({
|
|
731
|
+
candidates: new Set(["winner", "loser"]),
|
|
732
|
+
priorState: null,
|
|
733
|
+
userText: "u",
|
|
734
|
+
assistantText: "a",
|
|
735
|
+
nowText: "",
|
|
736
|
+
config,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
expect(out.breakdown.get("loser")?.simUserRerankBoost).toBe(0);
|
|
740
|
+
expect(out.breakdown.get("loser")?.simAssistantRerankBoost).toBe(0);
|
|
741
|
+
expect(out.breakdown.get("winner")?.simUserRerankBoost).toBeGreaterThan(0);
|
|
742
|
+
expect(
|
|
743
|
+
out.breakdown.get("winner")?.simAssistantRerankBoost,
|
|
744
|
+
).toBeGreaterThan(0);
|
|
745
|
+
// inRerankPool tags pool membership independently of the boost value, so
|
|
746
|
+
// the inspector can keep the rerank rows visible even when the channel
|
|
747
|
+
// max happened to normalise to 0.
|
|
748
|
+
expect(out.breakdown.get("winner")?.inRerankPool).toBe(true);
|
|
749
|
+
expect(out.breakdown.get("loser")?.inRerankPool).toBe(false);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("inRerankPool is false for every slug when rerank is disabled", async () => {
|
|
753
|
+
stageHybridResponse([{ slug: "alice", denseScore: 0.5 }]);
|
|
754
|
+
stageHybridResponse([{ slug: "alice", denseScore: 0.4 }]);
|
|
755
|
+
stageHybridResponse([{ slug: "alice", denseScore: 0.2 }]);
|
|
756
|
+
|
|
757
|
+
// No `rerank` block at all → rerankCfg is undefined and the rerank
|
|
758
|
+
// branch never runs, so no slug is in the pool.
|
|
759
|
+
const out = await computeOwnActivation({
|
|
760
|
+
candidates: new Set(["alice"]),
|
|
761
|
+
priorState: null,
|
|
762
|
+
userText: "u",
|
|
763
|
+
assistantText: "a",
|
|
764
|
+
nowText: "n",
|
|
765
|
+
config: makeConfig(),
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
expect(out.breakdown.get("alice")?.inRerankPool).toBe(false);
|
|
769
|
+
expect(out.breakdown.get("alice")?.simUserRerankBoost).toBe(0);
|
|
770
|
+
expect(out.breakdown.get("alice")?.simAssistantRerankBoost).toBe(0);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("rerank boost is additive on A_o and leaves raw simUser / simAssistant untouched", async () => {
|
|
774
|
+
stageHybridResponse([{ slug: "a", denseScore: 0.5 }]); // user
|
|
775
|
+
stageHybridResponse([{ slug: "a", denseScore: 0.4 }]); // assistant
|
|
776
|
+
stageHybridResponse([]); // now
|
|
777
|
+
rerankState.scores = new Map([["a", 0.8]]);
|
|
778
|
+
|
|
779
|
+
const config = {
|
|
780
|
+
memory: {
|
|
781
|
+
v2: {
|
|
782
|
+
d: 0.0,
|
|
783
|
+
c_user: 0.5,
|
|
784
|
+
c_assistant: 0.5,
|
|
785
|
+
c_now: 0.0,
|
|
786
|
+
dense_weight: 1.0,
|
|
787
|
+
sparse_weight: 0.0,
|
|
788
|
+
rerank: {
|
|
789
|
+
enabled: true,
|
|
790
|
+
top_k: 50,
|
|
791
|
+
alpha: 0.4,
|
|
792
|
+
model: "test-model",
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
} as unknown as AssistantConfig;
|
|
797
|
+
|
|
798
|
+
const out = await computeOwnActivation({
|
|
799
|
+
candidates: new Set(["a"]),
|
|
800
|
+
priorState: null,
|
|
801
|
+
userText: "u",
|
|
802
|
+
assistantText: "a",
|
|
803
|
+
nowText: "",
|
|
804
|
+
config,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const breakdown = out.breakdown.get("a");
|
|
808
|
+
// Raw fused similarities are reported untouched by rerank.
|
|
809
|
+
expect(breakdown?.simUser).toBeCloseTo(0.5, 6);
|
|
810
|
+
expect(breakdown?.simAssistant).toBeCloseTo(0.4, 6);
|
|
811
|
+
// Both rerank deltas are alpha · r_norm = 0.4 · 1.0 = 0.4 (single
|
|
812
|
+
// candidate normalises to 1.0 in each channel).
|
|
813
|
+
expect(breakdown?.simUserRerankBoost).toBeCloseTo(0.4, 6);
|
|
814
|
+
expect(breakdown?.simAssistantRerankBoost).toBeCloseTo(0.4, 6);
|
|
815
|
+
// Final A_o = c_user·simU + c_assistant·simA + c_user·boostU + c_assistant·boostA
|
|
816
|
+
// = 0.5·0.5 + 0.5·0.4 + 0.5·0.4 + 0.5·0.4 = 0.25+0.20+0.20+0.20 = 0.85
|
|
817
|
+
expect(out.activation.get("a")).toBeCloseTo(0.85, 6);
|
|
818
|
+
});
|
|
557
819
|
});
|
|
558
820
|
|
|
559
821
|
// ---------------------------------------------------------------------------
|
|
@@ -895,48 +1157,26 @@ describe("selectInjections", () => {
|
|
|
895
1157
|
});
|
|
896
1158
|
|
|
897
1159
|
// ---------------------------------------------------------------------------
|
|
898
|
-
//
|
|
1160
|
+
// Skills as concept slugs — the unified pool
|
|
899
1161
|
// ---------------------------------------------------------------------------
|
|
1162
|
+
//
|
|
1163
|
+
// Skills participate in the concept-page pipeline under the slug prefix
|
|
1164
|
+
// `skills/<id>`. There is no longer a dedicated skill activation function;
|
|
1165
|
+
// the only post-unification behavioral assertion worth preserving here is
|
|
1166
|
+
// that a `skills/<id>` slug flows through `computeOwnActivation` exactly
|
|
1167
|
+
// like a concept slug — same formula, same clamp, same breakdown shape.
|
|
1168
|
+
|
|
1169
|
+
describe("skills participate in the unified pipeline", () => {
|
|
1170
|
+
test("computeOwnActivation scores a `skills/<id>` slug like any concept slug", async () => {
|
|
1171
|
+
// Three simBatch responses, one per channel (user/assistant/now), with
|
|
1172
|
+
// a single skill-prefixed slug as the only candidate.
|
|
1173
|
+
stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.5 }]);
|
|
1174
|
+
stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.4 }]);
|
|
1175
|
+
stageHybridResponse([{ slug: "skills/example-skill-a", denseScore: 0.2 }]);
|
|
900
1176
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
): void {
|
|
905
|
-
state.skillQueryResponses.dense.push({
|
|
906
|
-
points: hits
|
|
907
|
-
.filter((h) => h.denseScore !== undefined)
|
|
908
|
-
.map((h) => ({ score: h.denseScore, payload: { id: h.id } })),
|
|
909
|
-
});
|
|
910
|
-
state.skillQueryResponses.sparse.push({
|
|
911
|
-
points: hits
|
|
912
|
-
.filter((h) => h.sparseScore !== undefined)
|
|
913
|
-
.map((h) => ({ score: h.sparseScore, payload: { id: h.id } })),
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
describe("computeSkillActivation", () => {
|
|
918
|
-
test("empty candidates short-circuits without backend calls", async () => {
|
|
919
|
-
const out = await computeSkillActivation({
|
|
920
|
-
candidates: new Set(),
|
|
921
|
-
userText: "u",
|
|
922
|
-
assistantText: "a",
|
|
923
|
-
nowText: "n",
|
|
924
|
-
config: makeConfig(),
|
|
925
|
-
});
|
|
926
|
-
expect(out.activation.size).toBe(0);
|
|
927
|
-
expect(out.breakdown.size).toBe(0);
|
|
928
|
-
expect(state.embedCalls).toHaveLength(0);
|
|
929
|
-
expect(state.queryCalls).toHaveLength(0);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
test("applies similarity-only formula with no decay term", async () => {
|
|
933
|
-
// Stage three skill responses — one per `simSkillBatch` call.
|
|
934
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]); // simU
|
|
935
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.4 }]); // simA
|
|
936
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.2 }]); // simN
|
|
937
|
-
|
|
938
|
-
const out = await computeSkillActivation({
|
|
939
|
-
candidates: new Set(["example-skill-a"]),
|
|
1177
|
+
const out = await computeOwnActivation({
|
|
1178
|
+
candidates: new Set(["skills/example-skill-a"]),
|
|
1179
|
+
priorState: null,
|
|
940
1180
|
userText: "u",
|
|
941
1181
|
assistantText: "a",
|
|
942
1182
|
nowText: "n",
|
|
@@ -947,191 +1187,12 @@ describe("computeSkillActivation", () => {
|
|
|
947
1187
|
c_now: 0.2,
|
|
948
1188
|
}),
|
|
949
1189
|
});
|
|
950
|
-
// No `d · prev` term: 0.3*0.5 + 0.2*0.4 + 0.2*0.2 = 0.15 + 0.08 + 0.04 = 0.27
|
|
951
|
-
expect(out.activation.get("example-skill-a")).toBeCloseTo(0.27, 6);
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
test("output excludes any decay term — d coefficient is unused", async () => {
|
|
955
|
-
// The skill activation formula is `c_user·simU + c_assistant·simA +
|
|
956
|
-
// c_now·simN`. Run with d=0.9 and d=0.0 — if the implementation
|
|
957
|
-
// accidentally included a `d · prev` term, the two would diverge. The
|
|
958
|
-
// function has no priorState parameter, so prev=0; both runs must equal
|
|
959
|
-
// the d-free formula exactly. Stage three sim responses per run.
|
|
960
|
-
const stage = () => {
|
|
961
|
-
stageSkillHybridResponse([{ id: "alpha", denseScore: 0.4 }]);
|
|
962
|
-
stageSkillHybridResponse([{ id: "alpha", denseScore: 0.4 }]);
|
|
963
|
-
stageSkillHybridResponse([{ id: "alpha", denseScore: 0.4 }]);
|
|
964
|
-
};
|
|
965
|
-
const baseConfig = { c_user: 0.3, c_assistant: 0.2, c_now: 0.2 };
|
|
966
|
-
|
|
967
|
-
stage();
|
|
968
|
-
const withHighD = await computeSkillActivation({
|
|
969
|
-
candidates: new Set(["alpha"]),
|
|
970
|
-
userText: "u",
|
|
971
|
-
assistantText: "a",
|
|
972
|
-
nowText: "n",
|
|
973
|
-
config: makeConfig({ ...baseConfig, d: 0.9 }),
|
|
974
|
-
});
|
|
975
|
-
stage();
|
|
976
|
-
const withZeroD = await computeSkillActivation({
|
|
977
|
-
candidates: new Set(["alpha"]),
|
|
978
|
-
userText: "u",
|
|
979
|
-
assistantText: "a",
|
|
980
|
-
nowText: "n",
|
|
981
|
-
config: makeConfig({ ...baseConfig, d: 0.0 }),
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
// Both equal `0.3*0.4 + 0.2*0.4 + 0.2*0.4 = 0.28` — d is ignored.
|
|
985
|
-
expect(withHighD.activation.get("alpha")).toBeCloseTo(0.28, 6);
|
|
986
|
-
expect(withZeroD.activation.get("alpha")).toBeCloseTo(0.28, 6);
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
test("clamps over-1.0 results down to [0, 1]", async () => {
|
|
990
|
-
stageSkillHybridResponse([{ id: "loud-skill", denseScore: 1.0 }]); // simU
|
|
991
|
-
stageSkillHybridResponse([{ id: "loud-skill", denseScore: 1.0 }]); // simA
|
|
992
|
-
stageSkillHybridResponse([{ id: "loud-skill", denseScore: 1.0 }]); // simN
|
|
993
|
-
|
|
994
|
-
// Coefficients intentionally sum to > 1 so the unclamped result
|
|
995
|
-
// overshoots — the implementation must still produce <= 1.0.
|
|
996
|
-
const out = await computeSkillActivation({
|
|
997
|
-
candidates: new Set(["loud-skill"]),
|
|
998
|
-
userText: "u",
|
|
999
|
-
assistantText: "a",
|
|
1000
|
-
nowText: "n",
|
|
1001
|
-
config: makeConfig({
|
|
1002
|
-
c_user: 0.5,
|
|
1003
|
-
c_assistant: 0.5,
|
|
1004
|
-
c_now: 0.5,
|
|
1005
|
-
}),
|
|
1006
|
-
});
|
|
1007
|
-
expect(out.activation.get("loud-skill")).toBe(1);
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
test("candidate with no sim hits resolves to 0", async () => {
|
|
1011
|
-
stageSkillHybridResponse([]);
|
|
1012
|
-
stageSkillHybridResponse([]);
|
|
1013
|
-
stageSkillHybridResponse([]);
|
|
1014
|
-
|
|
1015
|
-
const out = await computeSkillActivation({
|
|
1016
|
-
candidates: new Set(["ghost-skill"]),
|
|
1017
|
-
userText: "u",
|
|
1018
|
-
assistantText: "a",
|
|
1019
|
-
nowText: "n",
|
|
1020
|
-
config: makeConfig(),
|
|
1021
|
-
});
|
|
1022
|
-
expect(out.activation.get("ghost-skill")).toBe(0);
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
test("breakdown captures the raw sims for each candidate", async () => {
|
|
1026
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]); // simU
|
|
1027
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.4 }]); // simA
|
|
1028
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.2 }]); // simN
|
|
1029
|
-
|
|
1030
|
-
const out = await computeSkillActivation({
|
|
1031
|
-
candidates: new Set(["example-skill-a"]),
|
|
1032
|
-
userText: "u",
|
|
1033
|
-
assistantText: "a",
|
|
1034
|
-
nowText: "n",
|
|
1035
|
-
config: makeConfig({
|
|
1036
|
-
c_user: 0.3,
|
|
1037
|
-
c_assistant: 0.2,
|
|
1038
|
-
c_now: 0.2,
|
|
1039
|
-
}),
|
|
1040
|
-
});
|
|
1041
|
-
const breakdown = out.breakdown.get("example-skill-a");
|
|
1042
|
-
expect(breakdown).toBeDefined();
|
|
1043
|
-
expect(breakdown?.simUser).toBeCloseTo(0.5, 6);
|
|
1044
|
-
expect(breakdown?.simAssistant).toBeCloseTo(0.4, 6);
|
|
1045
|
-
expect(breakdown?.simNow).toBeCloseTo(0.2, 6);
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
test("uses the dedicated skills collection and never queries concept pages", async () => {
|
|
1049
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]);
|
|
1050
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]);
|
|
1051
|
-
stageSkillHybridResponse([{ id: "example-skill-a", denseScore: 0.5 }]);
|
|
1052
|
-
|
|
1053
|
-
await computeSkillActivation({
|
|
1054
|
-
candidates: new Set(["example-skill-a"]),
|
|
1055
|
-
userText: "u",
|
|
1056
|
-
assistantText: "a",
|
|
1057
|
-
nowText: "n",
|
|
1058
|
-
config: makeConfig(),
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
// Three simSkillBatch calls × 2 channels = 6 total queries, all against
|
|
1062
|
-
// the skills collection. No spread → no extra calls beyond these.
|
|
1063
|
-
expect(state.queryCalls).toHaveLength(6);
|
|
1064
|
-
for (const call of state.queryCalls) {
|
|
1065
|
-
expect(call.collection).toBe("memory_v2_skills");
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
// ---------------------------------------------------------------------------
|
|
1071
|
-
// selectSkillInjections
|
|
1072
|
-
// ---------------------------------------------------------------------------
|
|
1073
|
-
|
|
1074
|
-
describe("selectSkillInjections", () => {
|
|
1075
|
-
test("returns empty when activation is empty", () => {
|
|
1076
|
-
const out = selectSkillInjections({ A: new Map(), topK: 5 });
|
|
1077
|
-
expect(out).toEqual({ topNow: [] });
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
test("returns empty when topK is 0", () => {
|
|
1081
|
-
const out = selectSkillInjections({
|
|
1082
|
-
A: new Map([
|
|
1083
|
-
["example-skill-a", 0.5],
|
|
1084
|
-
["example-skill-b", 0.4],
|
|
1085
|
-
]),
|
|
1086
|
-
topK: 0,
|
|
1087
|
-
});
|
|
1088
|
-
expect(out).toEqual({ topNow: [] });
|
|
1089
|
-
});
|
|
1090
1190
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
["example-skill-d", 0.3],
|
|
1098
|
-
]),
|
|
1099
|
-
topK: 2,
|
|
1100
|
-
});
|
|
1101
|
-
expect(out.topNow).toEqual(["example-skill-b", "example-skill-c"]);
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
test("skills are stateless: the same id may be returned on consecutive turns", () => {
|
|
1105
|
-
// No `everInjected` parameter exists — selectSkillInjections takes only
|
|
1106
|
-
// the activation map and topK. So calling it twice with the same A map
|
|
1107
|
-
// returns the same result; there is no dedup against prior turns.
|
|
1108
|
-
const A = new Map([
|
|
1109
|
-
["example-skill-a", 0.9],
|
|
1110
|
-
["example-skill-b", 0.5],
|
|
1111
|
-
]);
|
|
1112
|
-
const turn1 = selectSkillInjections({ A, topK: 5 });
|
|
1113
|
-
const turn2 = selectSkillInjections({ A, topK: 5 });
|
|
1114
|
-
expect(turn1.topNow).toEqual(["example-skill-a", "example-skill-b"]);
|
|
1115
|
-
expect(turn2.topNow).toEqual(turn1.topNow);
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
test("breaks ties by id ascending for deterministic output", () => {
|
|
1119
|
-
const out = selectSkillInjections({
|
|
1120
|
-
A: new Map([
|
|
1121
|
-
["zeta-skill", 0.5],
|
|
1122
|
-
["example-skill-a", 0.5],
|
|
1123
|
-
["mike-skill", 0.5],
|
|
1124
|
-
]),
|
|
1125
|
-
topK: 5,
|
|
1126
|
-
});
|
|
1127
|
-
expect(out.topNow).toEqual(["example-skill-a", "mike-skill", "zeta-skill"]);
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
test("topK clamps to the available activation entries", () => {
|
|
1131
|
-
const out = selectSkillInjections({
|
|
1132
|
-
A: new Map([["only-skill", 0.7]]),
|
|
1133
|
-
topK: 100,
|
|
1134
|
-
});
|
|
1135
|
-
expect(out.topNow).toEqual(["only-skill"]);
|
|
1191
|
+
// No prior state → priorContribution = 0.
|
|
1192
|
+
// 0.3*0.5 + 0.2*0.4 + 0.2*0.2 = 0.15 + 0.08 + 0.04 = 0.27
|
|
1193
|
+
expect(out.activation.get("skills/example-skill-a")).toBeCloseTo(0.27, 6);
|
|
1194
|
+
expect(out.breakdown.get("skills/example-skill-a")?.priorContribution).toBe(
|
|
1195
|
+
0,
|
|
1196
|
+
);
|
|
1136
1197
|
});
|
|
1137
1198
|
});
|
|
@@ -372,26 +372,58 @@ describe("memoryV2ConsolidateJob — concurrent invocations", () => {
|
|
|
372
372
|
writeFileSync(bufferPath(), "- [Apr 27, 9:00 AM] Alice prefers VS Code.\n");
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
-
test("a
|
|
376
|
-
// Pre-seed a lock file
|
|
377
|
-
//
|
|
378
|
-
//
|
|
375
|
+
test("a live lock holder blocks a second concurrent invocation", async () => {
|
|
376
|
+
// Pre-seed a lock file with the current process's PID so the liveness
|
|
377
|
+
// probe sees a running holder and the second invocation correctly
|
|
378
|
+
// reports `locked` rather than taking over.
|
|
379
379
|
mkdirSync(join(memoryDir(), ".v2-state"), { recursive: true });
|
|
380
|
-
writeFileSync(lockPath(),
|
|
380
|
+
writeFileSync(lockPath(), `${process.pid} 1700000000000\n`);
|
|
381
381
|
|
|
382
382
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
383
383
|
|
|
384
384
|
expect(result.kind).toBe("locked");
|
|
385
385
|
if (result.kind === "locked") {
|
|
386
|
-
expect(result.holder).toContain(
|
|
386
|
+
expect(result.holder).toContain(`${process.pid}`);
|
|
387
387
|
}
|
|
388
388
|
expect(bootstrapCalls).toBe(0);
|
|
389
389
|
expect(wakeCalls).toBe(0);
|
|
390
390
|
expect(enqueuedJobs).toHaveLength(0);
|
|
391
|
-
// The
|
|
392
|
-
// owner releases it.
|
|
391
|
+
// The live holder's lock must NOT be removed by a contender.
|
|
393
392
|
expect(existsSync(lockPath())).toBe(true);
|
|
394
393
|
});
|
|
394
|
+
|
|
395
|
+
test("a stale lock from a non-running PID is taken over and consolidation proceeds", async () => {
|
|
396
|
+
// PID 999999 is well outside the typical kernel max_pid range on macOS
|
|
397
|
+
// and Linux, so kill(pid, 0) reliably returns ESRCH. The takeover path
|
|
398
|
+
// must unlink the stale file, retry the wx create, and bootstrap the
|
|
399
|
+
// background conversation as if the lock had been free all along.
|
|
400
|
+
mkdirSync(join(memoryDir(), ".v2-state"), { recursive: true });
|
|
401
|
+
writeFileSync(lockPath(), "999999 1700000000000\n");
|
|
402
|
+
|
|
403
|
+
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
404
|
+
|
|
405
|
+
expect(result.kind).toBe("invoked");
|
|
406
|
+
expect(bootstrapCalls).toBe(1);
|
|
407
|
+
expect(wakeCalls).toBe(1);
|
|
408
|
+
// Lock is released in the finally block after a successful run.
|
|
409
|
+
expect(existsSync(lockPath())).toBe(false);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("an empty / corrupted lock file is treated as stale and taken over", async () => {
|
|
413
|
+
// A zero-byte file simulates a prior holder that crashed between the
|
|
414
|
+
// O_EXCL create and the PID write. With only one writer ever, an
|
|
415
|
+
// unparseable payload is unambiguously corruption, not a live
|
|
416
|
+
// mid-write — take it over.
|
|
417
|
+
mkdirSync(join(memoryDir(), ".v2-state"), { recursive: true });
|
|
418
|
+
writeFileSync(lockPath(), "");
|
|
419
|
+
|
|
420
|
+
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
421
|
+
|
|
422
|
+
expect(result.kind).toBe("invoked");
|
|
423
|
+
expect(bootstrapCalls).toBe(1);
|
|
424
|
+
expect(wakeCalls).toBe(1);
|
|
425
|
+
expect(existsSync(lockPath())).toBe(false);
|
|
426
|
+
});
|
|
395
427
|
});
|
|
396
428
|
|
|
397
429
|
describe("CONSOLIDATION_PROMPT", () => {
|