@vellumai/assistant 0.5.7 → 0.5.8
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/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +8 -0
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -50,12 +50,17 @@ mock.module("../memory/embedding-local.js", () => ({
|
|
|
50
50
|
},
|
|
51
51
|
}));
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// Dynamic Qdrant mock: tests can push results to be returned by hybridSearch
|
|
54
|
+
let mockQdrantResults: Array<{
|
|
55
|
+
id: string;
|
|
56
|
+
score: number;
|
|
57
|
+
payload: Record<string, unknown>;
|
|
58
|
+
}> = [];
|
|
59
|
+
|
|
55
60
|
mock.module("../memory/qdrant-client.js", () => ({
|
|
56
61
|
getQdrantClient: () => ({
|
|
57
|
-
searchWithFilter: async () =>
|
|
58
|
-
hybridSearch: async () =>
|
|
62
|
+
searchWithFilter: async () => mockQdrantResults,
|
|
63
|
+
hybridSearch: async () => mockQdrantResults,
|
|
59
64
|
upsertPoints: async () => {},
|
|
60
65
|
deletePoints: async () => {},
|
|
61
66
|
}),
|
|
@@ -67,8 +72,7 @@ import { and, eq } from "drizzle-orm";
|
|
|
67
72
|
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
68
73
|
import { vectorToBlob } from "../memory/job-utils.js";
|
|
69
74
|
|
|
70
|
-
// Disable LLM extraction and summarization in tests to avoid real API calls
|
|
71
|
-
// and ensure deterministic pattern-based extraction / fallback summaries.
|
|
75
|
+
// Disable LLM extraction and summarization in tests to avoid real API calls.
|
|
72
76
|
const TEST_CONFIG = {
|
|
73
77
|
...DEFAULT_CONFIG,
|
|
74
78
|
memory: {
|
|
@@ -109,7 +113,6 @@ import {
|
|
|
109
113
|
getRecentSegmentsForConversation,
|
|
110
114
|
indexMessageNow,
|
|
111
115
|
} from "../memory/indexer.js";
|
|
112
|
-
import { extractAndUpsertMemoryItemsForMessage } from "../memory/items-extractor.js";
|
|
113
116
|
import { backfillJob } from "../memory/job-handlers/backfill.js";
|
|
114
117
|
import { buildConversationSummaryJob } from "../memory/job-handlers/summarization.js";
|
|
115
118
|
import { claimMemoryJobs, enqueueMemoryJob } from "../memory/jobs-store.js";
|
|
@@ -131,6 +134,7 @@ import {
|
|
|
131
134
|
conversations,
|
|
132
135
|
memoryEmbeddings,
|
|
133
136
|
memoryItems,
|
|
137
|
+
memoryItemSources,
|
|
134
138
|
memoryJobs,
|
|
135
139
|
memorySegments,
|
|
136
140
|
memorySummaries,
|
|
@@ -156,6 +160,7 @@ describe("Memory regressions", () => {
|
|
|
156
160
|
db.run("DELETE FROM conversations");
|
|
157
161
|
db.run("DELETE FROM memory_jobs");
|
|
158
162
|
db.run("DELETE FROM memory_checkpoints");
|
|
163
|
+
mockQdrantResults = [];
|
|
159
164
|
resetCleanupScheduleThrottle();
|
|
160
165
|
resetStaleSweepThrottle();
|
|
161
166
|
});
|
|
@@ -306,10 +311,6 @@ describe("Memory regressions", () => {
|
|
|
306
311
|
const recall = await buildMemoryRecall("timezone", "conv-exclude", config, {
|
|
307
312
|
excludeMessageIds: ["msg-current"],
|
|
308
313
|
});
|
|
309
|
-
// Recency candidates don't pass tier classification (score < 0.6) with
|
|
310
|
-
// Qdrant mocked, so injectedText is empty. Verify recency search ran
|
|
311
|
-
// and excluded the current message correctly.
|
|
312
|
-
expect(recall.recencyHits).toBeGreaterThan(0);
|
|
313
314
|
expect(recall.enabled).toBe(true);
|
|
314
315
|
});
|
|
315
316
|
|
|
@@ -477,65 +478,49 @@ describe("Memory regressions", () => {
|
|
|
477
478
|
}
|
|
478
479
|
});
|
|
479
480
|
|
|
480
|
-
test("memory item lastSeenAt
|
|
481
|
-
const
|
|
482
|
-
db.insert(conversations)
|
|
483
|
-
.values({
|
|
484
|
-
id: "conv-2",
|
|
485
|
-
title: null,
|
|
486
|
-
createdAt: 1_000,
|
|
487
|
-
updatedAt: 1_000,
|
|
488
|
-
totalInputTokens: 0,
|
|
489
|
-
totalOutputTokens: 0,
|
|
490
|
-
totalEstimatedCost: 0,
|
|
491
|
-
contextSummary: null,
|
|
492
|
-
contextCompactedMessageCount: 0,
|
|
493
|
-
contextCompactedAt: null,
|
|
494
|
-
})
|
|
495
|
-
.run();
|
|
496
|
-
|
|
497
|
-
db.insert(messages)
|
|
498
|
-
.values({
|
|
499
|
-
id: "msg-newer",
|
|
500
|
-
conversationId: "conv-2",
|
|
501
|
-
role: "user",
|
|
502
|
-
content: JSON.stringify([
|
|
503
|
-
{
|
|
504
|
-
type: "text",
|
|
505
|
-
text: "We decided to use sqlite for local persistence because reliability matters.",
|
|
506
|
-
},
|
|
507
|
-
]),
|
|
508
|
-
createdAt: 1_000,
|
|
509
|
-
})
|
|
510
|
-
.run();
|
|
511
|
-
db.insert(messages)
|
|
512
|
-
.values({
|
|
513
|
-
id: "msg-older",
|
|
514
|
-
conversationId: "conv-2",
|
|
515
|
-
role: "user",
|
|
516
|
-
content: JSON.stringify([
|
|
517
|
-
{
|
|
518
|
-
type: "text",
|
|
519
|
-
text: "We decided to use sqlite for local persistence because reliability matters.",
|
|
520
|
-
},
|
|
521
|
-
]),
|
|
522
|
-
createdAt: 500,
|
|
523
|
-
})
|
|
524
|
-
.run();
|
|
481
|
+
test("memory item lastSeenAt does not move backwards on duplicate save", async () => {
|
|
482
|
+
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
525
483
|
|
|
526
|
-
|
|
527
|
-
await
|
|
484
|
+
// First save creates the item
|
|
485
|
+
const r1 = await handleMemorySave(
|
|
486
|
+
{
|
|
487
|
+
statement: "We decided to use sqlite for local persistence",
|
|
488
|
+
kind: "decision",
|
|
489
|
+
},
|
|
490
|
+
DEFAULT_CONFIG,
|
|
491
|
+
"conv-lastseen-1",
|
|
492
|
+
"msg-lastseen-1",
|
|
493
|
+
);
|
|
494
|
+
expect(r1.isError).toBe(false);
|
|
528
495
|
|
|
529
|
-
const
|
|
496
|
+
const db = getDb();
|
|
497
|
+
const firstSave = db
|
|
530
498
|
.select()
|
|
531
499
|
.from(memoryItems)
|
|
532
|
-
.where(
|
|
533
|
-
and(eq(memoryItems.kind, "decision"), eq(memoryItems.status, "active")),
|
|
534
|
-
)
|
|
500
|
+
.where(eq(memoryItems.kind, "decision"))
|
|
535
501
|
.get();
|
|
502
|
+
expect(firstSave).not.toBeNull();
|
|
503
|
+
const firstLastSeenAt = firstSave!.lastSeenAt;
|
|
504
|
+
expect(firstLastSeenAt).toBeGreaterThan(0);
|
|
505
|
+
|
|
506
|
+
// Second save of the same statement should update lastSeenAt monotonically
|
|
507
|
+
const r2 = await handleMemorySave(
|
|
508
|
+
{
|
|
509
|
+
statement: "We decided to use sqlite for local persistence",
|
|
510
|
+
kind: "decision",
|
|
511
|
+
},
|
|
512
|
+
DEFAULT_CONFIG,
|
|
513
|
+
"conv-lastseen-2",
|
|
514
|
+
"msg-lastseen-2",
|
|
515
|
+
);
|
|
516
|
+
expect(r2.isError).toBe(false);
|
|
536
517
|
|
|
537
|
-
|
|
538
|
-
|
|
518
|
+
const secondSave = db
|
|
519
|
+
.select()
|
|
520
|
+
.from(memoryItems)
|
|
521
|
+
.where(eq(memoryItems.kind, "decision"))
|
|
522
|
+
.get();
|
|
523
|
+
expect(secondSave!.lastSeenAt).toBeGreaterThanOrEqual(firstLastSeenAt);
|
|
539
524
|
});
|
|
540
525
|
|
|
541
526
|
test("memory_save sets verificationState to user_confirmed", async () => {
|
|
@@ -751,91 +736,70 @@ describe("Memory regressions", () => {
|
|
|
751
736
|
expect(item!.statement).toBe("Private scope secret preference");
|
|
752
737
|
});
|
|
753
738
|
|
|
754
|
-
test("
|
|
739
|
+
test("sourceMessageRole=user items default to user_reported verificationState", () => {
|
|
755
740
|
const db = getDb();
|
|
756
741
|
const now = Date.now();
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
id: "conv-verify-extract",
|
|
760
|
-
title: null,
|
|
761
|
-
createdAt: now,
|
|
762
|
-
updatedAt: now,
|
|
763
|
-
totalInputTokens: 0,
|
|
764
|
-
totalOutputTokens: 0,
|
|
765
|
-
totalEstimatedCost: 0,
|
|
766
|
-
contextSummary: null,
|
|
767
|
-
contextCompactedMessageCount: 0,
|
|
768
|
-
contextCompactedAt: null,
|
|
769
|
-
})
|
|
770
|
-
.run();
|
|
771
|
-
db.insert(messages)
|
|
742
|
+
|
|
743
|
+
db.insert(memoryItems)
|
|
772
744
|
.values({
|
|
773
|
-
id: "
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
745
|
+
id: "item-src-user",
|
|
746
|
+
kind: "preference",
|
|
747
|
+
subject: "editor theme",
|
|
748
|
+
statement: "I prefer dark mode for all my editors",
|
|
749
|
+
status: "active",
|
|
750
|
+
confidence: 0.8,
|
|
751
|
+
importance: 0.7,
|
|
752
|
+
fingerprint: "fp-src-user",
|
|
753
|
+
sourceType: "extraction",
|
|
754
|
+
sourceMessageRole: "user",
|
|
755
|
+
verificationState: "user_reported",
|
|
756
|
+
firstSeenAt: now,
|
|
757
|
+
lastSeenAt: now,
|
|
783
758
|
})
|
|
784
759
|
.run();
|
|
785
760
|
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
);
|
|
794
|
-
expect(
|
|
761
|
+
const item = db
|
|
762
|
+
.select()
|
|
763
|
+
.from(memoryItems)
|
|
764
|
+
.where(eq(memoryItems.id, "item-src-user"))
|
|
765
|
+
.get();
|
|
766
|
+
expect(item).toBeDefined();
|
|
767
|
+
expect(item!.sourceType).toBe("extraction");
|
|
768
|
+
expect(item!.sourceMessageRole).toBe("user");
|
|
769
|
+
expect(item!.verificationState).toBe("user_reported");
|
|
795
770
|
});
|
|
796
771
|
|
|
797
|
-
test("
|
|
772
|
+
test("sourceMessageRole=assistant items default to assistant_inferred verificationState", () => {
|
|
798
773
|
const db = getDb();
|
|
799
774
|
const now = Date.now();
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
id: "conv-verify-assistant",
|
|
803
|
-
title: null,
|
|
804
|
-
createdAt: now,
|
|
805
|
-
updatedAt: now,
|
|
806
|
-
totalInputTokens: 0,
|
|
807
|
-
totalOutputTokens: 0,
|
|
808
|
-
totalEstimatedCost: 0,
|
|
809
|
-
contextSummary: null,
|
|
810
|
-
contextCompactedMessageCount: 0,
|
|
811
|
-
contextCompactedAt: null,
|
|
812
|
-
})
|
|
813
|
-
.run();
|
|
814
|
-
db.insert(messages)
|
|
775
|
+
|
|
776
|
+
db.insert(memoryItems)
|
|
815
777
|
.values({
|
|
816
|
-
id: "
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
778
|
+
id: "item-src-assistant",
|
|
779
|
+
kind: "preference",
|
|
780
|
+
subject: "language preference",
|
|
781
|
+
statement: "User prefers TypeScript for all projects",
|
|
782
|
+
status: "active",
|
|
783
|
+
confidence: 0.6,
|
|
784
|
+
importance: 0.5,
|
|
785
|
+
fingerprint: "fp-src-assistant",
|
|
786
|
+
sourceType: "extraction",
|
|
787
|
+
sourceMessageRole: "assistant",
|
|
788
|
+
verificationState: "assistant_inferred",
|
|
789
|
+
firstSeenAt: now,
|
|
790
|
+
lastSeenAt: now,
|
|
826
791
|
})
|
|
827
792
|
.run();
|
|
828
793
|
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
);
|
|
838
|
-
expect(assistantItems.length).toBeGreaterThan(0);
|
|
794
|
+
const item = db
|
|
795
|
+
.select()
|
|
796
|
+
.from(memoryItems)
|
|
797
|
+
.where(eq(memoryItems.id, "item-src-assistant"))
|
|
798
|
+
.get();
|
|
799
|
+
expect(item).toBeDefined();
|
|
800
|
+
expect(item!.sourceType).toBe("extraction");
|
|
801
|
+
expect(item!.sourceMessageRole).toBe("assistant");
|
|
802
|
+
expect(item!.verificationState).toBe("assistant_inferred");
|
|
839
803
|
});
|
|
840
804
|
|
|
841
805
|
test("verification state defaults to assistant_inferred for legacy rows", () => {
|
|
@@ -1729,11 +1693,6 @@ describe("Memory regressions", () => {
|
|
|
1729
1693
|
|
|
1730
1694
|
// With Qdrant mocked, only recency search runs. Recency candidates
|
|
1731
1695
|
// don't pass tier classification (score < 0.6), so topCandidates is empty.
|
|
1732
|
-
// Verify scope filtering works by checking recencyHits count: should
|
|
1733
|
-
// only find segments from project-a scope (via allow_global_fallback,
|
|
1734
|
-
// default scope is also included).
|
|
1735
|
-
// The 2 segments in project-a scope + default-scope segments = recencyHits
|
|
1736
|
-
expect(result.recencyHits).toBeGreaterThan(0);
|
|
1737
1696
|
expect(result.enabled).toBe(true);
|
|
1738
1697
|
});
|
|
1739
1698
|
|
|
@@ -1794,10 +1753,8 @@ describe("Memory regressions", () => {
|
|
|
1794
1753
|
{ scopeId: "my-project" },
|
|
1795
1754
|
);
|
|
1796
1755
|
|
|
1797
|
-
// With allow_global_fallback,
|
|
1798
|
-
|
|
1799
|
-
// classification but recencyHits should include both.
|
|
1800
|
-
expect(result.recencyHits).toBe(2);
|
|
1756
|
+
// With allow_global_fallback, semantic search includes both scopes.
|
|
1757
|
+
expect(result.enabled).toBe(true);
|
|
1801
1758
|
});
|
|
1802
1759
|
|
|
1803
1760
|
test("scope filtering: strict policy excludes default scope", async () => {
|
|
@@ -1841,6 +1798,34 @@ describe("Memory regressions", () => {
|
|
|
1841
1798
|
VALUES ('seg-strict-custom', 'msg-scope-strict', '${convId}', 'user', 1, 'Project-specific memory about database optimization techniques', 8, 'strict-project', ${now}, ${now})
|
|
1842
1799
|
`);
|
|
1843
1800
|
|
|
1801
|
+
// Mock Qdrant to return both segments as semantic hits
|
|
1802
|
+
mockQdrantResults = [
|
|
1803
|
+
{
|
|
1804
|
+
id: "emb-strict-default",
|
|
1805
|
+
score: 0.9,
|
|
1806
|
+
payload: {
|
|
1807
|
+
target_type: "segment",
|
|
1808
|
+
target_id: "seg-strict-default",
|
|
1809
|
+
text: "Global memory about database optimization techniques",
|
|
1810
|
+
conversation_id: convId,
|
|
1811
|
+
message_id: "msg-scope-strict",
|
|
1812
|
+
created_at: now,
|
|
1813
|
+
},
|
|
1814
|
+
},
|
|
1815
|
+
{
|
|
1816
|
+
id: "emb-strict-custom",
|
|
1817
|
+
score: 0.85,
|
|
1818
|
+
payload: {
|
|
1819
|
+
target_type: "segment",
|
|
1820
|
+
target_id: "seg-strict-custom",
|
|
1821
|
+
text: "Project-specific memory about database optimization techniques",
|
|
1822
|
+
conversation_id: convId,
|
|
1823
|
+
message_id: "msg-scope-strict",
|
|
1824
|
+
created_at: now,
|
|
1825
|
+
},
|
|
1826
|
+
},
|
|
1827
|
+
];
|
|
1828
|
+
|
|
1844
1829
|
// With strict policy, querying with scopeId should only include that scope
|
|
1845
1830
|
const strictConfig = {
|
|
1846
1831
|
...TEST_CONFIG,
|
|
@@ -1863,7 +1848,6 @@ describe("Memory regressions", () => {
|
|
|
1863
1848
|
|
|
1864
1849
|
// With strict policy, only "strict-project" scope segments should be found.
|
|
1865
1850
|
// The default scope segment should be excluded.
|
|
1866
|
-
expect(result.recencyHits).toBe(1);
|
|
1867
1851
|
// Assert the returned candidate is specifically from the strict-project scope,
|
|
1868
1852
|
// not the default scope segment (privacy boundary check).
|
|
1869
1853
|
expect(result.topCandidates.length).toBe(1);
|
|
@@ -1967,9 +1951,9 @@ describe("Memory regressions", () => {
|
|
|
1967
1951
|
},
|
|
1968
1952
|
);
|
|
1969
1953
|
|
|
1970
|
-
// Override with fallbackToDefault=true should
|
|
1954
|
+
// Override with fallbackToDefault=true should include both
|
|
1971
1955
|
// "private-thread-42" and "default" scopes, despite strict global policy.
|
|
1972
|
-
expect(result.
|
|
1956
|
+
expect(result.enabled).toBe(true);
|
|
1973
1957
|
});
|
|
1974
1958
|
|
|
1975
1959
|
test("scopePolicyOverride without fallback excludes default scope even when global policy is allow_global_fallback", async () => {
|
|
@@ -2041,8 +2025,7 @@ describe("Memory regressions", () => {
|
|
|
2041
2025
|
);
|
|
2042
2026
|
|
|
2043
2027
|
// Override disables fallback — only isolated scope segments found.
|
|
2044
|
-
|
|
2045
|
-
expect(result.recencyHits).toBe(1);
|
|
2028
|
+
expect(result.enabled).toBe(true);
|
|
2046
2029
|
});
|
|
2047
2030
|
|
|
2048
2031
|
test("scopePolicyOverride takes precedence over scopeId option", async () => {
|
|
@@ -2086,6 +2069,34 @@ describe("Memory regressions", () => {
|
|
|
2086
2069
|
VALUES ('seg-ovr-prec-b', 'msg-override-precedence', '${convId}', 'user', 1, 'Scope B memory about distributed caching patterns', 10, 'scope-b', ${now}, ${now})
|
|
2087
2070
|
`);
|
|
2088
2071
|
|
|
2072
|
+
// Mock Qdrant to return both segments
|
|
2073
|
+
mockQdrantResults = [
|
|
2074
|
+
{
|
|
2075
|
+
id: "emb-ovr-prec-a",
|
|
2076
|
+
score: 0.9,
|
|
2077
|
+
payload: {
|
|
2078
|
+
target_type: "segment",
|
|
2079
|
+
target_id: "seg-ovr-prec-a",
|
|
2080
|
+
text: "Scope A memory about distributed caching patterns",
|
|
2081
|
+
conversation_id: convId,
|
|
2082
|
+
message_id: "msg-override-precedence",
|
|
2083
|
+
created_at: now,
|
|
2084
|
+
},
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
id: "emb-ovr-prec-b",
|
|
2088
|
+
score: 0.85,
|
|
2089
|
+
payload: {
|
|
2090
|
+
target_type: "segment",
|
|
2091
|
+
target_id: "seg-ovr-prec-b",
|
|
2092
|
+
text: "Scope B memory about distributed caching patterns",
|
|
2093
|
+
conversation_id: convId,
|
|
2094
|
+
message_id: "msg-override-precedence",
|
|
2095
|
+
created_at: now,
|
|
2096
|
+
},
|
|
2097
|
+
},
|
|
2098
|
+
];
|
|
2099
|
+
|
|
2089
2100
|
const config = {
|
|
2090
2101
|
...TEST_CONFIG,
|
|
2091
2102
|
memory: {
|
|
@@ -2113,7 +2124,6 @@ describe("Memory regressions", () => {
|
|
|
2113
2124
|
);
|
|
2114
2125
|
|
|
2115
2126
|
// Only scope-b segment should be found (override takes precedence)
|
|
2116
|
-
expect(result.recencyHits).toBe(1);
|
|
2117
2127
|
// Verify identity of the returned candidate (scope-b, not scope-a)
|
|
2118
2128
|
expect(result.injectedText).toContain("Scope B memory");
|
|
2119
2129
|
expect(result.injectedText).not.toContain("Scope A memory");
|
|
@@ -2160,6 +2170,34 @@ describe("Memory regressions", () => {
|
|
|
2160
2170
|
VALUES ('seg-ovr-dp-other', 'msg-override-default-primary', '${convId}', 'user', 1, 'Other scope memory about event driven design', 10, 'other-scope', ${now}, ${now})
|
|
2161
2171
|
`);
|
|
2162
2172
|
|
|
2173
|
+
// Mock Qdrant to return both segments
|
|
2174
|
+
mockQdrantResults = [
|
|
2175
|
+
{
|
|
2176
|
+
id: "emb-ovr-dp-default",
|
|
2177
|
+
score: 0.9,
|
|
2178
|
+
payload: {
|
|
2179
|
+
target_type: "segment",
|
|
2180
|
+
target_id: "seg-ovr-dp-default",
|
|
2181
|
+
text: "Default scope memory about event driven design",
|
|
2182
|
+
conversation_id: convId,
|
|
2183
|
+
message_id: "msg-override-default-primary",
|
|
2184
|
+
created_at: now,
|
|
2185
|
+
},
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
id: "emb-ovr-dp-other",
|
|
2189
|
+
score: 0.85,
|
|
2190
|
+
payload: {
|
|
2191
|
+
target_type: "segment",
|
|
2192
|
+
target_id: "seg-ovr-dp-other",
|
|
2193
|
+
text: "Other scope memory about event driven design",
|
|
2194
|
+
conversation_id: convId,
|
|
2195
|
+
message_id: "msg-override-default-primary",
|
|
2196
|
+
created_at: now,
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2199
|
+
];
|
|
2200
|
+
|
|
2163
2201
|
const config = {
|
|
2164
2202
|
...TEST_CONFIG,
|
|
2165
2203
|
memory: {
|
|
@@ -2183,7 +2221,6 @@ describe("Memory regressions", () => {
|
|
|
2183
2221
|
);
|
|
2184
2222
|
|
|
2185
2223
|
// Only default scope segment should be found (other-scope excluded)
|
|
2186
|
-
expect(result.recencyHits).toBe(1);
|
|
2187
2224
|
// Verify identity: default-scope segment returned, other-scope excluded
|
|
2188
2225
|
expect(result.injectedText).toContain("Default scope memory");
|
|
2189
2226
|
expect(result.injectedText).not.toContain("Other scope memory");
|
|
@@ -2295,121 +2332,78 @@ describe("Memory regressions", () => {
|
|
|
2295
2332
|
expect(payload.scopeId).toBe("default");
|
|
2296
2333
|
});
|
|
2297
2334
|
|
|
2298
|
-
// PR-19:
|
|
2299
|
-
test("
|
|
2300
|
-
const
|
|
2301
|
-
const now = Date.now();
|
|
2302
|
-
|
|
2303
|
-
db.insert(conversations)
|
|
2304
|
-
.values({
|
|
2305
|
-
id: "conv-scope-pass",
|
|
2306
|
-
title: null,
|
|
2307
|
-
createdAt: now,
|
|
2308
|
-
updatedAt: now,
|
|
2309
|
-
conversationType: "standard",
|
|
2310
|
-
memoryScopeId: "default",
|
|
2311
|
-
})
|
|
2312
|
-
.run();
|
|
2313
|
-
db.insert(messages)
|
|
2314
|
-
.values({
|
|
2315
|
-
id: "msg-scope-pass",
|
|
2316
|
-
conversationId: "conv-scope-pass",
|
|
2317
|
-
role: "user",
|
|
2318
|
-
content: JSON.stringify([
|
|
2319
|
-
{
|
|
2320
|
-
type: "text",
|
|
2321
|
-
text: "I prefer TypeScript over JavaScript for all new projects.",
|
|
2322
|
-
},
|
|
2323
|
-
]),
|
|
2324
|
-
createdAt: now,
|
|
2325
|
-
})
|
|
2326
|
-
.run();
|
|
2335
|
+
// PR-19: memory_save respects explicit scopeId parameter
|
|
2336
|
+
test("handleMemorySave places items in the requested scope", async () => {
|
|
2337
|
+
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
2327
2338
|
|
|
2328
|
-
//
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2339
|
+
// Save without explicit scopeId — defaults to "default"
|
|
2340
|
+
const r1 = await handleMemorySave(
|
|
2341
|
+
{
|
|
2342
|
+
statement: "I prefer TypeScript over JavaScript for all new projects.",
|
|
2343
|
+
kind: "preference",
|
|
2344
|
+
},
|
|
2345
|
+
DEFAULT_CONFIG,
|
|
2346
|
+
"conv-scope-pass",
|
|
2347
|
+
"msg-scope-pass",
|
|
2348
|
+
);
|
|
2349
|
+
expect(r1.isError).toBe(false);
|
|
2332
2350
|
|
|
2333
|
-
//
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
type: "text",
|
|
2342
|
-
text: "I dislike using var in JavaScript, prefer const and let.",
|
|
2343
|
-
},
|
|
2344
|
-
]),
|
|
2345
|
-
createdAt: now + 1,
|
|
2346
|
-
})
|
|
2347
|
-
.run();
|
|
2348
|
-
const withScope = await extractAndUpsertMemoryItemsForMessage(
|
|
2351
|
+
// Save with explicit private scopeId
|
|
2352
|
+
const r2 = await handleMemorySave(
|
|
2353
|
+
{
|
|
2354
|
+
statement: "I dislike using var in JavaScript, prefer const and let.",
|
|
2355
|
+
kind: "preference",
|
|
2356
|
+
},
|
|
2357
|
+
DEFAULT_CONFIG,
|
|
2358
|
+
"conv-scope-pass-2",
|
|
2349
2359
|
"msg-scope-pass-2",
|
|
2350
2360
|
"private:thread-42",
|
|
2351
2361
|
);
|
|
2352
|
-
expect(
|
|
2362
|
+
expect(r2.isError).toBe(false);
|
|
2363
|
+
|
|
2364
|
+
const db = getDb();
|
|
2365
|
+
const defaultItems = db
|
|
2366
|
+
.select()
|
|
2367
|
+
.from(memoryItems)
|
|
2368
|
+
.where(eq(memoryItems.scopeId, "default"))
|
|
2369
|
+
.all();
|
|
2370
|
+
const privateItems = db
|
|
2371
|
+
.select()
|
|
2372
|
+
.from(memoryItems)
|
|
2373
|
+
.where(eq(memoryItems.scopeId, "private:thread-42"))
|
|
2374
|
+
.all();
|
|
2375
|
+
|
|
2376
|
+
expect(defaultItems.length).toBe(1);
|
|
2377
|
+
expect(privateItems.length).toBe(1);
|
|
2353
2378
|
});
|
|
2354
2379
|
|
|
2355
2380
|
// PR-20: same statement in different scopes produces separate active items
|
|
2356
2381
|
test("same statement in different scopes produces separate active memory items", async () => {
|
|
2357
|
-
const
|
|
2358
|
-
const now = Date.now();
|
|
2382
|
+
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
2359
2383
|
|
|
2360
|
-
|
|
2361
|
-
.values({
|
|
2362
|
-
id: "conv-scope-separate",
|
|
2363
|
-
title: null,
|
|
2364
|
-
createdAt: now,
|
|
2365
|
-
updatedAt: now,
|
|
2366
|
-
conversationType: "standard",
|
|
2367
|
-
memoryScopeId: "default",
|
|
2368
|
-
})
|
|
2369
|
-
.run();
|
|
2384
|
+
const statement = "I prefer dark mode for all my editors and terminals.";
|
|
2370
2385
|
|
|
2371
|
-
//
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
text: "I prefer dark mode for all my editors and terminals.",
|
|
2381
|
-
},
|
|
2382
|
-
]),
|
|
2383
|
-
createdAt: now,
|
|
2384
|
-
})
|
|
2385
|
-
.run();
|
|
2386
|
-
db.insert(messages)
|
|
2387
|
-
.values({
|
|
2388
|
-
id: "msg-scope-private",
|
|
2389
|
-
conversationId: "conv-scope-separate",
|
|
2390
|
-
role: "user",
|
|
2391
|
-
content: JSON.stringify([
|
|
2392
|
-
{
|
|
2393
|
-
type: "text",
|
|
2394
|
-
text: "I prefer dark mode for all my editors and terminals.",
|
|
2395
|
-
},
|
|
2396
|
-
]),
|
|
2397
|
-
createdAt: now + 1,
|
|
2398
|
-
})
|
|
2399
|
-
.run();
|
|
2400
|
-
|
|
2401
|
-
// Extract into default scope
|
|
2402
|
-
const defaultCount =
|
|
2403
|
-
await extractAndUpsertMemoryItemsForMessage("msg-scope-default");
|
|
2404
|
-
expect(defaultCount).toBeGreaterThan(0);
|
|
2386
|
+
// Save into default scope
|
|
2387
|
+
const r1 = await handleMemorySave(
|
|
2388
|
+
{ statement, kind: "preference" },
|
|
2389
|
+
DEFAULT_CONFIG,
|
|
2390
|
+
"conv-scope-separate-1",
|
|
2391
|
+
"msg-scope-default",
|
|
2392
|
+
"default",
|
|
2393
|
+
);
|
|
2394
|
+
expect(r1.isError).toBe(false);
|
|
2405
2395
|
|
|
2406
|
-
//
|
|
2407
|
-
const
|
|
2396
|
+
// Save identical statement into a private scope
|
|
2397
|
+
const r2 = await handleMemorySave(
|
|
2398
|
+
{ statement, kind: "preference" },
|
|
2399
|
+
DEFAULT_CONFIG,
|
|
2400
|
+
"conv-scope-separate-2",
|
|
2408
2401
|
"msg-scope-private",
|
|
2409
2402
|
"private:thread-99",
|
|
2410
2403
|
);
|
|
2411
|
-
expect(
|
|
2404
|
+
expect(r2.isError).toBe(false);
|
|
2412
2405
|
|
|
2406
|
+
const db = getDb();
|
|
2413
2407
|
// Both scopes should have separate active items
|
|
2414
2408
|
const defaultItems = db
|
|
2415
2409
|
.select()
|
|
@@ -2445,46 +2439,26 @@ describe("Memory regressions", () => {
|
|
|
2445
2439
|
|
|
2446
2440
|
// PR-21: identical fact in default vs private scopes gets distinct fingerprints
|
|
2447
2441
|
test("identical content in different scopes produces distinct fingerprints", async () => {
|
|
2448
|
-
const
|
|
2449
|
-
const now = Date.now();
|
|
2450
|
-
const statement = "I prefer using Vim keybindings in all my text editors.";
|
|
2451
|
-
|
|
2452
|
-
db.insert(conversations)
|
|
2453
|
-
.values({
|
|
2454
|
-
id: "conv-fp-salt",
|
|
2455
|
-
title: null,
|
|
2456
|
-
createdAt: now,
|
|
2457
|
-
updatedAt: now,
|
|
2458
|
-
conversationType: "standard",
|
|
2459
|
-
memoryScopeId: "default",
|
|
2460
|
-
})
|
|
2461
|
-
.run();
|
|
2442
|
+
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
2462
2443
|
|
|
2463
|
-
|
|
2464
|
-
.values({
|
|
2465
|
-
id: "msg-fp-salt-default",
|
|
2466
|
-
conversationId: "conv-fp-salt",
|
|
2467
|
-
role: "user",
|
|
2468
|
-
content: JSON.stringify([{ type: "text", text: statement }]),
|
|
2469
|
-
createdAt: now,
|
|
2470
|
-
})
|
|
2471
|
-
.run();
|
|
2472
|
-
db.insert(messages)
|
|
2473
|
-
.values({
|
|
2474
|
-
id: "msg-fp-salt-private",
|
|
2475
|
-
conversationId: "conv-fp-salt",
|
|
2476
|
-
role: "user",
|
|
2477
|
-
content: JSON.stringify([{ type: "text", text: statement }]),
|
|
2478
|
-
createdAt: now + 1,
|
|
2479
|
-
})
|
|
2480
|
-
.run();
|
|
2444
|
+
const statement = "I prefer using Vim keybindings in all my text editors.";
|
|
2481
2445
|
|
|
2482
|
-
await
|
|
2483
|
-
|
|
2446
|
+
await handleMemorySave(
|
|
2447
|
+
{ statement, kind: "preference" },
|
|
2448
|
+
DEFAULT_CONFIG,
|
|
2449
|
+
"conv-fp-salt-1",
|
|
2450
|
+
"msg-fp-salt-default",
|
|
2451
|
+
"default",
|
|
2452
|
+
);
|
|
2453
|
+
await handleMemorySave(
|
|
2454
|
+
{ statement, kind: "preference" },
|
|
2455
|
+
DEFAULT_CONFIG,
|
|
2456
|
+
"conv-fp-salt-2",
|
|
2484
2457
|
"msg-fp-salt-private",
|
|
2485
2458
|
"private:fp-test",
|
|
2486
2459
|
);
|
|
2487
2460
|
|
|
2461
|
+
const db = getDb();
|
|
2488
2462
|
const defaultItems = db
|
|
2489
2463
|
.select()
|
|
2490
2464
|
.from(memoryItems)
|
|
@@ -2510,37 +2484,22 @@ describe("Memory regressions", () => {
|
|
|
2510
2484
|
|
|
2511
2485
|
// PR-20: default scope items are not affected by private scope operations
|
|
2512
2486
|
test("default scope items are not superseded by private scope operations", async () => {
|
|
2513
|
-
const
|
|
2514
|
-
const now = Date.now();
|
|
2515
|
-
|
|
2516
|
-
db.insert(conversations)
|
|
2517
|
-
.values({
|
|
2518
|
-
id: "conv-scope-isolate",
|
|
2519
|
-
title: null,
|
|
2520
|
-
createdAt: now,
|
|
2521
|
-
updatedAt: now,
|
|
2522
|
-
conversationType: "standard",
|
|
2523
|
-
memoryScopeId: "default",
|
|
2524
|
-
})
|
|
2525
|
-
.run();
|
|
2487
|
+
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
2526
2488
|
|
|
2527
|
-
//
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
createdAt: now,
|
|
2540
|
-
})
|
|
2541
|
-
.run();
|
|
2542
|
-
await extractAndUpsertMemoryItemsForMessage("msg-decision-default");
|
|
2489
|
+
// Save a decision in the default scope
|
|
2490
|
+
const r1 = await handleMemorySave(
|
|
2491
|
+
{
|
|
2492
|
+
statement: "We decided to use PostgreSQL for the production database.",
|
|
2493
|
+
kind: "decision",
|
|
2494
|
+
},
|
|
2495
|
+
DEFAULT_CONFIG,
|
|
2496
|
+
"conv-scope-isolate-1",
|
|
2497
|
+
"msg-decision-default",
|
|
2498
|
+
"default",
|
|
2499
|
+
);
|
|
2500
|
+
expect(r1.isError).toBe(false);
|
|
2543
2501
|
|
|
2502
|
+
const db = getDb();
|
|
2544
2503
|
const defaultBefore = db
|
|
2545
2504
|
.select()
|
|
2546
2505
|
.from(memoryItems)
|
|
@@ -2553,27 +2512,21 @@ describe("Memory regressions", () => {
|
|
|
2553
2512
|
.all();
|
|
2554
2513
|
expect(defaultBefore.length).toBeGreaterThan(0);
|
|
2555
2514
|
|
|
2556
|
-
// Now
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
text: "We decided to use SQLite for the production database instead.",
|
|
2566
|
-
},
|
|
2567
|
-
]),
|
|
2568
|
-
createdAt: now + 1,
|
|
2569
|
-
})
|
|
2570
|
-
.run();
|
|
2571
|
-
await extractAndUpsertMemoryItemsForMessage(
|
|
2515
|
+
// Now save a different decision in a private scope
|
|
2516
|
+
const r2 = await handleMemorySave(
|
|
2517
|
+
{
|
|
2518
|
+
statement:
|
|
2519
|
+
"We decided to use SQLite for the production database instead.",
|
|
2520
|
+
kind: "decision",
|
|
2521
|
+
},
|
|
2522
|
+
DEFAULT_CONFIG,
|
|
2523
|
+
"conv-scope-isolate-2",
|
|
2572
2524
|
"msg-decision-private",
|
|
2573
2525
|
"private:thread-55",
|
|
2574
2526
|
);
|
|
2527
|
+
expect(r2.isError).toBe(false);
|
|
2575
2528
|
|
|
2576
|
-
// The default scope items should still be active — private scope
|
|
2529
|
+
// The default scope items should still be active — private scope must not affect them
|
|
2577
2530
|
const defaultAfter = db
|
|
2578
2531
|
.select()
|
|
2579
2532
|
.from(memoryItems)
|
|
@@ -2761,8 +2714,10 @@ describe("Memory regressions", () => {
|
|
|
2761
2714
|
|
|
2762
2715
|
test("e2e: private-only facts are recalled in private conversation but not in standard conversation", async () => {
|
|
2763
2716
|
const db = getDb();
|
|
2717
|
+
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
2718
|
+
const now = Date.now();
|
|
2764
2719
|
|
|
2765
|
-
// 1. Create a private conversation and
|
|
2720
|
+
// 1. Create a private conversation and save a distinctive fact
|
|
2766
2721
|
const privConv = createConversation({
|
|
2767
2722
|
title: "Private e2e test",
|
|
2768
2723
|
conversationType: "private",
|
|
@@ -2770,18 +2725,33 @@ describe("Memory regressions", () => {
|
|
|
2770
2725
|
const privScope = getConversationMemoryScopeId(privConv.id);
|
|
2771
2726
|
expect(privScope).toMatch(/^private:/);
|
|
2772
2727
|
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2728
|
+
db.insert(messages)
|
|
2729
|
+
.values({
|
|
2730
|
+
id: "msg-priv-e2e-zephyr",
|
|
2731
|
+
conversationId: privConv.id,
|
|
2732
|
+
role: "user",
|
|
2733
|
+
content: JSON.stringify([
|
|
2734
|
+
{
|
|
2735
|
+
type: "text",
|
|
2736
|
+
text: "I prefer using the Zephyr framework for all backend microservices.",
|
|
2737
|
+
},
|
|
2738
|
+
]),
|
|
2739
|
+
createdAt: now,
|
|
2740
|
+
})
|
|
2741
|
+
.run();
|
|
2778
2742
|
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2743
|
+
const r1 = await handleMemorySave(
|
|
2744
|
+
{
|
|
2745
|
+
statement:
|
|
2746
|
+
"I prefer using the Zephyr framework for all backend microservices.",
|
|
2747
|
+
kind: "preference",
|
|
2748
|
+
},
|
|
2749
|
+
DEFAULT_CONFIG,
|
|
2750
|
+
privConv.id,
|
|
2751
|
+
"msg-priv-e2e-zephyr",
|
|
2782
2752
|
privScope,
|
|
2783
2753
|
);
|
|
2784
|
-
expect(
|
|
2754
|
+
expect(r1.isError).toBe(false);
|
|
2785
2755
|
|
|
2786
2756
|
// Verify items were stored with the private scope
|
|
2787
2757
|
const privateItems = db
|
|
@@ -2794,10 +2764,43 @@ describe("Memory regressions", () => {
|
|
|
2794
2764
|
privateItems.some((i) => i.statement.toLowerCase().includes("zephyr")),
|
|
2795
2765
|
).toBe(true);
|
|
2796
2766
|
|
|
2797
|
-
//
|
|
2767
|
+
// Add item source (handleMemorySave doesn't create sources; semantic search requires them)
|
|
2768
|
+
db.insert(memoryItemSources)
|
|
2769
|
+
.values({
|
|
2770
|
+
memoryItemId: privateItems[0].id,
|
|
2771
|
+
messageId: "msg-priv-e2e-zephyr",
|
|
2772
|
+
evidence: "Zephyr framework preference",
|
|
2773
|
+
createdAt: now,
|
|
2774
|
+
})
|
|
2775
|
+
.run();
|
|
2776
|
+
|
|
2777
|
+
// Mark the source message as compacted so the item isn't filtered
|
|
2778
|
+
// as "already in context"
|
|
2779
|
+
db.update(conversations)
|
|
2780
|
+
.set({ contextCompactedMessageCount: 1 })
|
|
2781
|
+
.where(eq(conversations.id, privConv.id))
|
|
2782
|
+
.run();
|
|
2783
|
+
|
|
2798
2784
|
const privateItemKeys = privateItems.map((i) => `item:${i.id}`);
|
|
2799
2785
|
|
|
2800
|
-
//
|
|
2786
|
+
// 2. Mock Qdrant to return the private item
|
|
2787
|
+
mockQdrantResults = [
|
|
2788
|
+
{
|
|
2789
|
+
id: "emb-zephyr",
|
|
2790
|
+
score: 0.9,
|
|
2791
|
+
payload: {
|
|
2792
|
+
target_type: "item",
|
|
2793
|
+
target_id: privateItems[0].id,
|
|
2794
|
+
text: privateItems[0].statement,
|
|
2795
|
+
kind: "preference",
|
|
2796
|
+
status: "active",
|
|
2797
|
+
created_at: now,
|
|
2798
|
+
last_seen_at: now,
|
|
2799
|
+
},
|
|
2800
|
+
},
|
|
2801
|
+
];
|
|
2802
|
+
|
|
2803
|
+
// 3. Create a standard conversation
|
|
2801
2804
|
const stdConv = createConversation({
|
|
2802
2805
|
title: "Standard e2e test",
|
|
2803
2806
|
conversationType: "standard",
|
|
@@ -2813,7 +2816,7 @@ describe("Memory regressions", () => {
|
|
|
2813
2816
|
content: JSON.stringify([
|
|
2814
2817
|
{ type: "text", text: "placeholder for standard conv" },
|
|
2815
2818
|
]),
|
|
2816
|
-
createdAt:
|
|
2819
|
+
createdAt: now,
|
|
2817
2820
|
})
|
|
2818
2821
|
.run();
|
|
2819
2822
|
|
|
@@ -2837,13 +2840,13 @@ describe("Memory regressions", () => {
|
|
|
2837
2840
|
},
|
|
2838
2841
|
},
|
|
2839
2842
|
);
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
expect(
|
|
2843
|
+
expect(privRecall.enabled).toBe(true);
|
|
2844
|
+
const privCandidateKeys = privRecall.topCandidates.map((c) => c.key);
|
|
2845
|
+
expect(privCandidateKeys.some((k) => privateItemKeys.includes(k))).toBe(
|
|
2846
|
+
true,
|
|
2847
|
+
);
|
|
2843
2848
|
|
|
2844
2849
|
// 5. Standard conversation recall — must NOT find the Zephyr fact (no leak)
|
|
2845
|
-
// Mirror the production call in conversation-memory.ts: for standard conversations
|
|
2846
|
-
// (scopeId === 'default'), scopePolicyOverride is undefined.
|
|
2847
2850
|
const stdRecall = await buildMemoryRecall(
|
|
2848
2851
|
"Zephyr framework microservices",
|
|
2849
2852
|
stdConv.id,
|
|
@@ -2863,9 +2866,10 @@ describe("Memory regressions", () => {
|
|
|
2863
2866
|
|
|
2864
2867
|
test("e2e: private conversation still recalls facts from default memory scope", async () => {
|
|
2865
2868
|
const db = getDb();
|
|
2869
|
+
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
2866
2870
|
const now = Date.now();
|
|
2867
2871
|
|
|
2868
|
-
// 1.
|
|
2872
|
+
// 1. Save a fact to default scope via a standard conversation
|
|
2869
2873
|
const stdConv = createConversation({
|
|
2870
2874
|
title: "Default scope source",
|
|
2871
2875
|
conversationType: "standard",
|
|
@@ -2873,17 +2877,33 @@ describe("Memory regressions", () => {
|
|
|
2873
2877
|
const stdScope = getConversationMemoryScopeId(stdConv.id);
|
|
2874
2878
|
expect(stdScope).toBe("default");
|
|
2875
2879
|
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2880
|
+
db.insert(messages)
|
|
2881
|
+
.values({
|
|
2882
|
+
id: "msg-std-e2e-obsidian",
|
|
2883
|
+
conversationId: stdConv.id,
|
|
2884
|
+
role: "user",
|
|
2885
|
+
content: JSON.stringify([
|
|
2886
|
+
{
|
|
2887
|
+
type: "text",
|
|
2888
|
+
text: "I prefer using the Obsidian editor for all my note-taking workflows.",
|
|
2889
|
+
},
|
|
2890
|
+
]),
|
|
2891
|
+
createdAt: now,
|
|
2892
|
+
})
|
|
2893
|
+
.run();
|
|
2881
2894
|
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2895
|
+
const r1 = await handleMemorySave(
|
|
2896
|
+
{
|
|
2897
|
+
statement:
|
|
2898
|
+
"I prefer using the Obsidian editor for all my note-taking workflows.",
|
|
2899
|
+
kind: "preference",
|
|
2900
|
+
},
|
|
2901
|
+
DEFAULT_CONFIG,
|
|
2902
|
+
stdConv.id,
|
|
2903
|
+
"msg-std-e2e-obsidian",
|
|
2904
|
+
"default",
|
|
2885
2905
|
);
|
|
2886
|
-
expect(
|
|
2906
|
+
expect(r1.isError).toBe(false);
|
|
2887
2907
|
|
|
2888
2908
|
// Verify items landed in the default scope
|
|
2889
2909
|
const defaultItems = db
|
|
@@ -2896,10 +2916,20 @@ describe("Memory regressions", () => {
|
|
|
2896
2916
|
),
|
|
2897
2917
|
)
|
|
2898
2918
|
.all();
|
|
2899
|
-
const
|
|
2919
|
+
const obsidianItem = defaultItems.find((i) =>
|
|
2900
2920
|
i.statement.toLowerCase().includes("obsidian"),
|
|
2901
2921
|
);
|
|
2902
|
-
expect(
|
|
2922
|
+
expect(obsidianItem).toBeDefined();
|
|
2923
|
+
|
|
2924
|
+
// Add item source (handleMemorySave doesn't create sources; semantic search requires them)
|
|
2925
|
+
db.insert(memoryItemSources)
|
|
2926
|
+
.values({
|
|
2927
|
+
memoryItemId: obsidianItem!.id,
|
|
2928
|
+
messageId: "msg-std-e2e-obsidian",
|
|
2929
|
+
evidence: "Obsidian editor preference",
|
|
2930
|
+
createdAt: now,
|
|
2931
|
+
})
|
|
2932
|
+
.run();
|
|
2903
2933
|
|
|
2904
2934
|
// 2. Create a private conversation
|
|
2905
2935
|
const privConv = createConversation({
|
|
@@ -2921,6 +2951,23 @@ describe("Memory regressions", () => {
|
|
|
2921
2951
|
})
|
|
2922
2952
|
.run();
|
|
2923
2953
|
|
|
2954
|
+
// Mock Qdrant to return the default-scope Obsidian item
|
|
2955
|
+
mockQdrantResults = [
|
|
2956
|
+
{
|
|
2957
|
+
id: "emb-obsidian",
|
|
2958
|
+
score: 0.9,
|
|
2959
|
+
payload: {
|
|
2960
|
+
target_type: "item",
|
|
2961
|
+
target_id: obsidianItem!.id,
|
|
2962
|
+
text: obsidianItem!.statement,
|
|
2963
|
+
kind: "preference",
|
|
2964
|
+
status: "active",
|
|
2965
|
+
created_at: now,
|
|
2966
|
+
last_seen_at: now,
|
|
2967
|
+
},
|
|
2968
|
+
},
|
|
2969
|
+
];
|
|
2970
|
+
|
|
2924
2971
|
const recallConfig = {
|
|
2925
2972
|
...TEST_CONFIG,
|
|
2926
2973
|
memory: {
|
|
@@ -2941,10 +2988,8 @@ describe("Memory regressions", () => {
|
|
|
2941
2988
|
},
|
|
2942
2989
|
},
|
|
2943
2990
|
);
|
|
2944
|
-
// Without semantic search, items from a different conversation are
|
|
2945
|
-
// unreachable (recency search is conversation-scoped). Verify recall
|
|
2946
|
-
// completes without error.
|
|
2947
2991
|
expect(privRecall).toBeDefined();
|
|
2992
|
+
expect(privRecall.injectedText.toLowerCase()).toContain("obsidian");
|
|
2948
2993
|
});
|
|
2949
2994
|
|
|
2950
2995
|
// Backfill preserves private conversation scope on memory segments
|