@vellumai/assistant 0.4.49 → 0.4.50
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
afterAll,
|
|
6
|
-
beforeAll,
|
|
7
|
-
beforeEach,
|
|
8
|
-
describe,
|
|
9
|
-
expect,
|
|
10
|
-
mock,
|
|
11
|
-
test,
|
|
12
|
-
} from "bun:test";
|
|
13
|
-
|
|
14
|
-
import { eq } from "drizzle-orm";
|
|
15
|
-
|
|
16
|
-
const testDir = mkdtempSync(join(tmpdir(), "contradiction-checker-test-"));
|
|
17
|
-
|
|
18
|
-
let nextRelationship = "ambiguous_contradiction";
|
|
19
|
-
let nextExplanation = "Statements likely conflict but need confirmation.";
|
|
20
|
-
let classifyCallCount = 0;
|
|
21
|
-
|
|
22
|
-
const classifyRelationshipMock = mock(async () => {
|
|
23
|
-
classifyCallCount += 1;
|
|
24
|
-
return {
|
|
25
|
-
content: [
|
|
26
|
-
{
|
|
27
|
-
type: "tool_use" as const,
|
|
28
|
-
id: "test-tool-use-id",
|
|
29
|
-
name: "classify_relationship",
|
|
30
|
-
input: {
|
|
31
|
-
relationship: nextRelationship,
|
|
32
|
-
explanation: nextExplanation,
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
model: "claude-haiku-4-5-20251001",
|
|
37
|
-
stopReason: "tool_use",
|
|
38
|
-
usage: { inputTokens: 0, outputTokens: 0 },
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
mock.module("../providers/provider-send-message.js", () => ({
|
|
43
|
-
getConfiguredProvider: () => ({
|
|
44
|
-
sendMessage: classifyRelationshipMock,
|
|
45
|
-
}),
|
|
46
|
-
createTimeout: (ms: number) => {
|
|
47
|
-
const controller = new AbortController();
|
|
48
|
-
const timer = setTimeout(() => controller.abort(), ms);
|
|
49
|
-
return {
|
|
50
|
-
signal: controller.signal,
|
|
51
|
-
cleanup: () => clearTimeout(timer),
|
|
52
|
-
};
|
|
53
|
-
},
|
|
54
|
-
extractToolUse: (response: { content: Array<{ type: string }> }) => {
|
|
55
|
-
return response.content.find(
|
|
56
|
-
(b: { type: string }) => b.type === "tool_use",
|
|
57
|
-
);
|
|
58
|
-
},
|
|
59
|
-
userMessage: (text: string) => ({
|
|
60
|
-
role: "user",
|
|
61
|
-
content: [{ type: "text", text }],
|
|
62
|
-
}),
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
mock.module("../util/platform.js", () => ({
|
|
66
|
-
getDataDir: () => testDir,
|
|
67
|
-
isMacOS: () => process.platform === "darwin",
|
|
68
|
-
isLinux: () => process.platform === "linux",
|
|
69
|
-
isWindows: () => process.platform === "win32",
|
|
70
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
71
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
72
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
73
|
-
ensureDataDir: () => {},
|
|
74
|
-
}));
|
|
75
|
-
|
|
76
|
-
mock.module("../util/logger.js", () => ({
|
|
77
|
-
getLogger: () =>
|
|
78
|
-
new Proxy({} as Record<string, unknown>, {
|
|
79
|
-
get: () => () => {},
|
|
80
|
-
}),
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
let mockConflictableKinds: string[] = [
|
|
84
|
-
"preference",
|
|
85
|
-
"profile",
|
|
86
|
-
"constraint",
|
|
87
|
-
"instruction",
|
|
88
|
-
"style",
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
mock.module("../config/loader.js", () => ({
|
|
92
|
-
getConfig: () => ({
|
|
93
|
-
ui: {},
|
|
94
|
-
|
|
95
|
-
apiKeys: { anthropic: "test-key" },
|
|
96
|
-
memory: {
|
|
97
|
-
conflicts: {
|
|
98
|
-
conflictableKinds: mockConflictableKinds,
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
}));
|
|
103
|
-
|
|
104
|
-
import { checkContradictions } from "../memory/contradiction-checker.js";
|
|
105
|
-
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
106
|
-
import { memoryItemConflicts, memoryItems } from "../memory/schema.js";
|
|
107
|
-
|
|
108
|
-
beforeAll(() => {
|
|
109
|
-
initializeDb();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
beforeEach(() => {
|
|
113
|
-
classifyCallCount = 0;
|
|
114
|
-
mockConflictableKinds = [
|
|
115
|
-
"preference",
|
|
116
|
-
"profile",
|
|
117
|
-
"constraint",
|
|
118
|
-
"instruction",
|
|
119
|
-
"style",
|
|
120
|
-
];
|
|
121
|
-
const db = getDb();
|
|
122
|
-
db.run("DELETE FROM memory_item_conflicts");
|
|
123
|
-
db.run("DELETE FROM memory_item_sources");
|
|
124
|
-
db.run("DELETE FROM memory_jobs");
|
|
125
|
-
db.run("DELETE FROM memory_items");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
afterAll(() => {
|
|
129
|
-
resetDb();
|
|
130
|
-
try {
|
|
131
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
132
|
-
} catch {
|
|
133
|
-
// best effort cleanup
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
function insertMemoryItem(params: {
|
|
138
|
-
id: string;
|
|
139
|
-
statement: string;
|
|
140
|
-
scopeId?: string;
|
|
141
|
-
status?: "active" | "pending_clarification";
|
|
142
|
-
kind?: string;
|
|
143
|
-
}): void {
|
|
144
|
-
const now = Date.now();
|
|
145
|
-
const db = getDb();
|
|
146
|
-
db.insert(memoryItems)
|
|
147
|
-
.values({
|
|
148
|
-
id: params.id,
|
|
149
|
-
kind: params.kind ?? "preference",
|
|
150
|
-
subject: "framework preference",
|
|
151
|
-
statement: params.statement,
|
|
152
|
-
status: params.status ?? "active",
|
|
153
|
-
confidence: 0.8,
|
|
154
|
-
importance: 0.7,
|
|
155
|
-
fingerprint: `fp-${params.id}`,
|
|
156
|
-
verificationState: "assistant_inferred",
|
|
157
|
-
scopeId: params.scopeId ?? "default",
|
|
158
|
-
firstSeenAt: now,
|
|
159
|
-
lastSeenAt: now,
|
|
160
|
-
})
|
|
161
|
-
.run();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
describe("checkContradictions", () => {
|
|
165
|
-
test("marks candidate pending and writes one conflict row for ambiguous contradictions", async () => {
|
|
166
|
-
nextRelationship = "ambiguous_contradiction";
|
|
167
|
-
nextExplanation = "Seems contradictory; ask user to choose.";
|
|
168
|
-
|
|
169
|
-
insertMemoryItem({
|
|
170
|
-
id: "item-existing-ambiguous",
|
|
171
|
-
statement: "User prefers React for frontend work.",
|
|
172
|
-
scopeId: "workspace-a",
|
|
173
|
-
});
|
|
174
|
-
insertMemoryItem({
|
|
175
|
-
id: "item-candidate-ambiguous",
|
|
176
|
-
statement: "User prefers Vue for frontend work.",
|
|
177
|
-
scopeId: "workspace-a",
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
await checkContradictions("item-candidate-ambiguous");
|
|
181
|
-
|
|
182
|
-
const db = getDb();
|
|
183
|
-
const candidate = db
|
|
184
|
-
.select()
|
|
185
|
-
.from(memoryItems)
|
|
186
|
-
.where(eq(memoryItems.id, "item-candidate-ambiguous"))
|
|
187
|
-
.get();
|
|
188
|
-
const existing = db
|
|
189
|
-
.select()
|
|
190
|
-
.from(memoryItems)
|
|
191
|
-
.where(eq(memoryItems.id, "item-existing-ambiguous"))
|
|
192
|
-
.get();
|
|
193
|
-
const conflicts = db.select().from(memoryItemConflicts).all();
|
|
194
|
-
|
|
195
|
-
expect(classifyCallCount).toBe(1);
|
|
196
|
-
expect(candidate?.status).toBe("pending_clarification");
|
|
197
|
-
expect(existing?.invalidAt).toBeNull();
|
|
198
|
-
expect(conflicts).toHaveLength(1);
|
|
199
|
-
expect(conflicts[0].status).toBe("pending_clarification");
|
|
200
|
-
expect(conflicts[0].existingItemId).toBe("item-existing-ambiguous");
|
|
201
|
-
expect(conflicts[0].candidateItemId).toBe("item-candidate-ambiguous");
|
|
202
|
-
expect(conflicts[0].relationship).toBe("ambiguous_contradiction");
|
|
203
|
-
expect(conflicts[0].clarificationQuestion).toContain("Pending conflict:");
|
|
204
|
-
expect(conflicts[0].clarificationQuestion).not.toContain(
|
|
205
|
-
"I have conflicting notes",
|
|
206
|
-
);
|
|
207
|
-
expect(conflicts[0].clarificationQuestion).not.toContain(
|
|
208
|
-
"Which one is correct?",
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("keeps existing contradiction behavior and does not create conflict row", async () => {
|
|
213
|
-
nextRelationship = "contradiction";
|
|
214
|
-
nextExplanation = "The statements are directly incompatible.";
|
|
215
|
-
|
|
216
|
-
insertMemoryItem({
|
|
217
|
-
id: "item-existing-contradiction",
|
|
218
|
-
statement: "User prefers dark mode.",
|
|
219
|
-
});
|
|
220
|
-
insertMemoryItem({
|
|
221
|
-
id: "item-candidate-contradiction",
|
|
222
|
-
statement: "User prefers light mode.",
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
await checkContradictions("item-candidate-contradiction");
|
|
226
|
-
|
|
227
|
-
const db = getDb();
|
|
228
|
-
const candidate = db
|
|
229
|
-
.select()
|
|
230
|
-
.from(memoryItems)
|
|
231
|
-
.where(eq(memoryItems.id, "item-candidate-contradiction"))
|
|
232
|
-
.get();
|
|
233
|
-
const existing = db
|
|
234
|
-
.select()
|
|
235
|
-
.from(memoryItems)
|
|
236
|
-
.where(eq(memoryItems.id, "item-existing-contradiction"))
|
|
237
|
-
.get();
|
|
238
|
-
const conflicts = db.select().from(memoryItemConflicts).all();
|
|
239
|
-
|
|
240
|
-
expect(classifyCallCount).toBe(1);
|
|
241
|
-
expect(candidate?.status).toBe("active");
|
|
242
|
-
expect(typeof candidate?.validFrom).toBe("number");
|
|
243
|
-
expect(typeof existing?.invalidAt).toBe("number");
|
|
244
|
-
expect(conflicts).toHaveLength(0);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test("only evaluates contradiction candidates within the same scope", async () => {
|
|
248
|
-
nextRelationship = "ambiguous_contradiction";
|
|
249
|
-
nextExplanation = "Should not be used for this test.";
|
|
250
|
-
|
|
251
|
-
insertMemoryItem({
|
|
252
|
-
id: "item-existing-other-scope",
|
|
253
|
-
statement: "Use Go for backend services.",
|
|
254
|
-
scopeId: "workspace-b",
|
|
255
|
-
});
|
|
256
|
-
insertMemoryItem({
|
|
257
|
-
id: "item-candidate-default-scope",
|
|
258
|
-
statement: "Use Rust for backend services.",
|
|
259
|
-
scopeId: "workspace-a",
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
await checkContradictions("item-candidate-default-scope");
|
|
263
|
-
|
|
264
|
-
const db = getDb();
|
|
265
|
-
const candidate = db
|
|
266
|
-
.select()
|
|
267
|
-
.from(memoryItems)
|
|
268
|
-
.where(eq(memoryItems.id, "item-candidate-default-scope"))
|
|
269
|
-
.get();
|
|
270
|
-
const conflicts = db.select().from(memoryItemConflicts).all();
|
|
271
|
-
|
|
272
|
-
expect(classifyCallCount).toBe(0);
|
|
273
|
-
expect(candidate?.status).toBe("active");
|
|
274
|
-
expect(conflicts).toHaveLength(0);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
test("project kind ambiguous contradiction does not generate pending conflict with default config", async () => {
|
|
278
|
-
nextRelationship = "ambiguous_contradiction";
|
|
279
|
-
nextExplanation = "Project items may conflict but are not durable.";
|
|
280
|
-
|
|
281
|
-
insertMemoryItem({
|
|
282
|
-
id: "item-existing-project",
|
|
283
|
-
statement: "The backend uses Node.js.",
|
|
284
|
-
kind: "project",
|
|
285
|
-
});
|
|
286
|
-
insertMemoryItem({
|
|
287
|
-
id: "item-candidate-project",
|
|
288
|
-
statement: "The backend uses Deno.",
|
|
289
|
-
kind: "project",
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await checkContradictions("item-candidate-project");
|
|
293
|
-
|
|
294
|
-
expect(classifyCallCount).toBe(0);
|
|
295
|
-
const db = getDb();
|
|
296
|
-
const conflicts = db.select().from(memoryItemConflicts).all();
|
|
297
|
-
expect(conflicts).toHaveLength(0);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("skips classification when item kind is not in conflictableKinds", async () => {
|
|
301
|
-
mockConflictableKinds = ["instruction", "style"];
|
|
302
|
-
nextRelationship = "ambiguous_contradiction";
|
|
303
|
-
|
|
304
|
-
insertMemoryItem({
|
|
305
|
-
id: "item-existing-ineligible",
|
|
306
|
-
statement: "User prefers React for frontend work.",
|
|
307
|
-
});
|
|
308
|
-
insertMemoryItem({
|
|
309
|
-
id: "item-candidate-ineligible",
|
|
310
|
-
statement: "User prefers Vue for frontend work.",
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
await checkContradictions("item-candidate-ineligible");
|
|
314
|
-
|
|
315
|
-
expect(classifyCallCount).toBe(0);
|
|
316
|
-
const db = getDb();
|
|
317
|
-
const conflicts = db.select().from(memoryItemConflicts).all();
|
|
318
|
-
expect(conflicts).toHaveLength(0);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test("skips classification when candidate statement contains PR-tracking content", async () => {
|
|
322
|
-
nextRelationship = "ambiguous_contradiction";
|
|
323
|
-
|
|
324
|
-
insertMemoryItem({
|
|
325
|
-
id: "item-existing-pr-tracking",
|
|
326
|
-
statement: "Track PR #5526 for review.",
|
|
327
|
-
});
|
|
328
|
-
insertMemoryItem({
|
|
329
|
-
id: "item-candidate-pr-tracking",
|
|
330
|
-
statement: "Track PR #5525 for review.",
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
await checkContradictions("item-candidate-pr-tracking");
|
|
334
|
-
|
|
335
|
-
expect(classifyCallCount).toBe(0);
|
|
336
|
-
const db = getDb();
|
|
337
|
-
const conflicts = db.select().from(memoryItemConflicts).all();
|
|
338
|
-
expect(conflicts).toHaveLength(0);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
test("durable preference contradiction still runs normal flow", async () => {
|
|
342
|
-
nextRelationship = "ambiguous_contradiction";
|
|
343
|
-
nextExplanation = "Both are valid preferences that conflict.";
|
|
344
|
-
|
|
345
|
-
insertMemoryItem({
|
|
346
|
-
id: "item-existing-durable",
|
|
347
|
-
statement: "User prefers React for frontend work.",
|
|
348
|
-
});
|
|
349
|
-
insertMemoryItem({
|
|
350
|
-
id: "item-candidate-durable",
|
|
351
|
-
statement: "User prefers Vue for frontend work.",
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
await checkContradictions("item-candidate-durable");
|
|
355
|
-
|
|
356
|
-
expect(classifyCallCount).toBe(1);
|
|
357
|
-
const db = getDb();
|
|
358
|
-
const conflicts = db.select().from(memoryItemConflicts).all();
|
|
359
|
-
expect(conflicts).toHaveLength(1);
|
|
360
|
-
});
|
|
361
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
afterAll,
|
|
6
|
-
beforeAll,
|
|
7
|
-
beforeEach,
|
|
8
|
-
describe,
|
|
9
|
-
expect,
|
|
10
|
-
mock,
|
|
11
|
-
test,
|
|
12
|
-
} from "bun:test";
|
|
13
|
-
|
|
14
|
-
import { and, eq } from "drizzle-orm";
|
|
15
|
-
|
|
16
|
-
const testDir = mkdtempSync(join(tmpdir(), "entity-extractor-test-"));
|
|
17
|
-
|
|
18
|
-
mock.module("../util/platform.js", () => ({
|
|
19
|
-
getDataDir: () => testDir,
|
|
20
|
-
isMacOS: () => process.platform === "darwin",
|
|
21
|
-
isLinux: () => process.platform === "linux",
|
|
22
|
-
isWindows: () => process.platform === "win32",
|
|
23
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
24
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
25
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
26
|
-
ensureDataDir: () => {},
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
mock.module("../util/logger.js", () => ({
|
|
30
|
-
getLogger: () =>
|
|
31
|
-
new Proxy({} as Record<string, unknown>, {
|
|
32
|
-
get: () => () => {},
|
|
33
|
-
}),
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
37
|
-
import {
|
|
38
|
-
resolveEntityName,
|
|
39
|
-
upsertEntity,
|
|
40
|
-
upsertEntityRelation,
|
|
41
|
-
} from "../memory/entity-extractor.js";
|
|
42
|
-
import { memoryEntities, memoryEntityRelations } from "../memory/schema.js";
|
|
43
|
-
|
|
44
|
-
describe("entity extractor helpers", () => {
|
|
45
|
-
beforeAll(() => {
|
|
46
|
-
initializeDb();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
const db = getDb();
|
|
51
|
-
db.run("DELETE FROM memory_item_entities");
|
|
52
|
-
db.run("DELETE FROM memory_entity_relations");
|
|
53
|
-
db.run("DELETE FROM memory_entities");
|
|
54
|
-
db.run("DELETE FROM memory_checkpoints");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
afterAll(() => {
|
|
58
|
-
resetDb();
|
|
59
|
-
try {
|
|
60
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
61
|
-
} catch {
|
|
62
|
-
// best effort cleanup
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("upsertEntity reuses existing row via alias matching", () => {
|
|
67
|
-
const db = getDb();
|
|
68
|
-
|
|
69
|
-
const firstId = upsertEntity({
|
|
70
|
-
name: "VS Code",
|
|
71
|
-
type: "tool",
|
|
72
|
-
aliases: ["vscode"],
|
|
73
|
-
});
|
|
74
|
-
const secondId = upsertEntity({
|
|
75
|
-
name: "Visual Studio Code",
|
|
76
|
-
type: "tool",
|
|
77
|
-
aliases: ["VS Code", "vscode"],
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
expect(secondId).toBe(firstId);
|
|
81
|
-
expect(resolveEntityName("vscode")).toBe(firstId);
|
|
82
|
-
expect(resolveEntityName("VS Code")).toBe(firstId);
|
|
83
|
-
|
|
84
|
-
const stored = db
|
|
85
|
-
.select()
|
|
86
|
-
.from(memoryEntities)
|
|
87
|
-
.where(eq(memoryEntities.id, firstId))
|
|
88
|
-
.get();
|
|
89
|
-
|
|
90
|
-
expect(stored).toBeDefined();
|
|
91
|
-
expect(stored!.mentionCount).toBe(2);
|
|
92
|
-
const aliases = stored!.aliases
|
|
93
|
-
? (JSON.parse(stored!.aliases) as string[])
|
|
94
|
-
: [];
|
|
95
|
-
expect(aliases).toContain("vscode");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("upsertEntityRelation merges duplicate edges by uniqueness key", () => {
|
|
99
|
-
const db = getDb();
|
|
100
|
-
const sourceEntityId = upsertEntity({
|
|
101
|
-
name: "Project Atlas",
|
|
102
|
-
type: "project",
|
|
103
|
-
aliases: ["atlas"],
|
|
104
|
-
});
|
|
105
|
-
const targetEntityId = upsertEntity({
|
|
106
|
-
name: "Qdrant",
|
|
107
|
-
type: "tool",
|
|
108
|
-
aliases: [],
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
upsertEntityRelation({
|
|
112
|
-
sourceEntityId,
|
|
113
|
-
targetEntityId,
|
|
114
|
-
relation: "uses",
|
|
115
|
-
evidence: "Project Atlas uses Qdrant for memory search",
|
|
116
|
-
seenAt: 1_700_000_000_000,
|
|
117
|
-
});
|
|
118
|
-
upsertEntityRelation({
|
|
119
|
-
sourceEntityId,
|
|
120
|
-
targetEntityId,
|
|
121
|
-
relation: "uses",
|
|
122
|
-
evidence: null,
|
|
123
|
-
seenAt: 1_700_000_100_000,
|
|
124
|
-
});
|
|
125
|
-
upsertEntityRelation({
|
|
126
|
-
sourceEntityId,
|
|
127
|
-
targetEntityId,
|
|
128
|
-
relation: "uses",
|
|
129
|
-
evidence: "Atlas still depends on Qdrant",
|
|
130
|
-
seenAt: 1_700_000_200_000,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const relationRows = db
|
|
134
|
-
.select()
|
|
135
|
-
.from(memoryEntityRelations)
|
|
136
|
-
.where(
|
|
137
|
-
and(
|
|
138
|
-
eq(memoryEntityRelations.sourceEntityId, sourceEntityId),
|
|
139
|
-
eq(memoryEntityRelations.targetEntityId, targetEntityId),
|
|
140
|
-
eq(memoryEntityRelations.relation, "uses"),
|
|
141
|
-
),
|
|
142
|
-
)
|
|
143
|
-
.all();
|
|
144
|
-
|
|
145
|
-
expect(relationRows.length).toBe(1);
|
|
146
|
-
expect(relationRows[0].firstSeenAt).toBe(1_700_000_000_000);
|
|
147
|
-
expect(relationRows[0].lastSeenAt).toBe(1_700_000_200_000);
|
|
148
|
-
expect(relationRows[0].evidence).toBe("Atlas still depends on Qdrant");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("resolveEntityName prefers exact canonical name over alias match", () => {
|
|
152
|
-
const db = getDb();
|
|
153
|
-
const now = Date.now();
|
|
154
|
-
|
|
155
|
-
// Insert two distinct entities directly to avoid upsertEntity dedupe.
|
|
156
|
-
const aliasEntityId = crypto.randomUUID();
|
|
157
|
-
db.insert(memoryEntities)
|
|
158
|
-
.values({
|
|
159
|
-
id: aliasEntityId,
|
|
160
|
-
name: "React Native",
|
|
161
|
-
type: "tool",
|
|
162
|
-
aliases: JSON.stringify(["React", "RN"]),
|
|
163
|
-
description: null,
|
|
164
|
-
firstSeenAt: now,
|
|
165
|
-
lastSeenAt: now,
|
|
166
|
-
mentionCount: 1,
|
|
167
|
-
})
|
|
168
|
-
.run();
|
|
169
|
-
|
|
170
|
-
const canonicalEntityId = crypto.randomUUID();
|
|
171
|
-
db.insert(memoryEntities)
|
|
172
|
-
.values({
|
|
173
|
-
id: canonicalEntityId,
|
|
174
|
-
name: "React",
|
|
175
|
-
type: "tool",
|
|
176
|
-
aliases: JSON.stringify(["ReactJS"]),
|
|
177
|
-
description: null,
|
|
178
|
-
firstSeenAt: now,
|
|
179
|
-
lastSeenAt: now,
|
|
180
|
-
mentionCount: 1,
|
|
181
|
-
})
|
|
182
|
-
.run();
|
|
183
|
-
|
|
184
|
-
// resolveEntityName("React") should return the entity whose canonical
|
|
185
|
-
// name is "React", not the one that merely lists it as an alias.
|
|
186
|
-
expect(resolveEntityName("React")).toBe(canonicalEntityId);
|
|
187
|
-
expect(resolveEntityName("react")).toBe(canonicalEntityId);
|
|
188
|
-
// Alias-only lookups still work
|
|
189
|
-
expect(resolveEntityName("RN")).toBe(aliasEntityId);
|
|
190
|
-
expect(resolveEntityName("ReactJS")).toBe(canonicalEntityId);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("upsertEntityRelation drops self-edges", () => {
|
|
194
|
-
const db = getDb();
|
|
195
|
-
const entityId = upsertEntity({
|
|
196
|
-
name: "Sidd",
|
|
197
|
-
type: "person",
|
|
198
|
-
aliases: [],
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
upsertEntityRelation({
|
|
202
|
-
sourceEntityId: entityId,
|
|
203
|
-
targetEntityId: entityId,
|
|
204
|
-
relation: "collaborates_with",
|
|
205
|
-
evidence: "self edge should not be stored",
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const relationRows = db.select().from(memoryEntityRelations).all();
|
|
209
|
-
expect(relationRows.length).toBe(0);
|
|
210
|
-
});
|
|
211
|
-
});
|