@vellumai/assistant 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/architecture/memory.md +105 -0
- package/package.json +1 -1
- package/src/__tests__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-types.test.ts +12 -4
- package/src/__tests__/memory-reducer.test.ts +7 -1
- package/src/__tests__/memory-regressions.test.ts +24 -4
- package/src/__tests__/memory-simplified-config.test.ts +4 -4
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/loader.ts +0 -1
- package/src/config/schemas/memory-simplified.ts +1 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/lifecycle.ts +44 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/brief-time.ts +5 -4
- package/src/memory/conversation-crud.ts +210 -0
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +4 -0
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +9 -3
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/jobs-store.ts +2 -0
- package/src/memory/jobs-worker.ts +8 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-types.ts +9 -2
- package/src/memory/reducer.ts +25 -11
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/runtime/auth/route-policy.ts +10 -1
- package/src/runtime/routes/conversation-management-routes.ts +88 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- package/src/runtime/routes/secret-routes.ts +1 -0
- package/src/schedule/schedule-store.ts +7 -0
- package/src/schedule/scheduler.ts +6 -2
- package/src/telemetry/usage-telemetry-reporter.ts +1 -1
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for the simplified memory system.
|
|
3
|
+
*
|
|
4
|
+
* Covers the must-have scenarios:
|
|
5
|
+
* 1. Backfill: legacy segments, summaries, and items migrate to simplified tables
|
|
6
|
+
* 2. Simplified memory is enabled by default after backfill
|
|
7
|
+
* 3. Memory tools use simplified path when enabled
|
|
8
|
+
* 4. Legacy tables remain available as rollback support
|
|
9
|
+
*/
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
afterAll,
|
|
15
|
+
beforeAll,
|
|
16
|
+
beforeEach,
|
|
17
|
+
describe,
|
|
18
|
+
expect,
|
|
19
|
+
mock,
|
|
20
|
+
test,
|
|
21
|
+
} from "bun:test";
|
|
22
|
+
|
|
23
|
+
const testDir = mkdtempSync(join(tmpdir(), "simplified-memory-e2e-test-"));
|
|
24
|
+
const dbPath = join(testDir, "test.db");
|
|
25
|
+
|
|
26
|
+
// ── Platform mock (must come before any module imports) ──────────────
|
|
27
|
+
|
|
28
|
+
mock.module("../util/platform.js", () => ({
|
|
29
|
+
getDataDir: () => testDir,
|
|
30
|
+
getRootDir: () => testDir,
|
|
31
|
+
isMacOS: () => process.platform === "darwin",
|
|
32
|
+
isLinux: () => process.platform === "linux",
|
|
33
|
+
isWindows: () => process.platform === "win32",
|
|
34
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
35
|
+
getDbPath: () => dbPath,
|
|
36
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
37
|
+
ensureDataDir: () => {},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
mock.module("../util/logger.js", () => ({
|
|
41
|
+
getLogger: () =>
|
|
42
|
+
new Proxy({} as Record<string, unknown>, {
|
|
43
|
+
get: () => () => {},
|
|
44
|
+
}),
|
|
45
|
+
truncateForLog: (value: string) => value,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Stub the Qdrant and embedding backends since we don't need vectors
|
|
49
|
+
mock.module("../memory/qdrant-client.js", () => ({
|
|
50
|
+
getQdrantClient: () => ({
|
|
51
|
+
searchWithFilter: async () => [],
|
|
52
|
+
hybridSearch: async () => [],
|
|
53
|
+
upsertPoints: async () => {},
|
|
54
|
+
deletePoints: async () => {},
|
|
55
|
+
upsert: async () => {},
|
|
56
|
+
}),
|
|
57
|
+
initQdrantClient: () => {},
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
mock.module("../memory/embedding-backend.js", () => ({
|
|
61
|
+
getMemoryBackendStatus: async () => ({
|
|
62
|
+
provider: null,
|
|
63
|
+
reason: "test-stub",
|
|
64
|
+
}),
|
|
65
|
+
embedWithBackend: async () => ({
|
|
66
|
+
vectors: [[]],
|
|
67
|
+
provider: "test",
|
|
68
|
+
model: "test",
|
|
69
|
+
}),
|
|
70
|
+
generateSparseEmbedding: () => undefined,
|
|
71
|
+
selectEmbeddingBackend: async () => null,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
mock.module("../memory/qdrant-circuit-breaker.js", () => ({
|
|
75
|
+
withQdrantBreaker: async (fn: () => Promise<unknown>) => fn(),
|
|
76
|
+
QdrantCircuitOpenError: class extends Error {},
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// ── Configurable config mock ────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
82
|
+
import type { AssistantConfig } from "../config/types.js";
|
|
83
|
+
|
|
84
|
+
let testConfig: AssistantConfig = {
|
|
85
|
+
...DEFAULT_CONFIG,
|
|
86
|
+
memory: {
|
|
87
|
+
...DEFAULT_CONFIG.memory,
|
|
88
|
+
enabled: true,
|
|
89
|
+
simplified: {
|
|
90
|
+
...DEFAULT_CONFIG.memory.simplified,
|
|
91
|
+
enabled: true,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
mock.module("../config/loader.js", () => ({
|
|
97
|
+
loadConfig: () => testConfig,
|
|
98
|
+
getConfig: () => testConfig,
|
|
99
|
+
loadRawConfig: () => ({}),
|
|
100
|
+
saveRawConfig: () => {},
|
|
101
|
+
invalidateConfigCache: () => {},
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// ── Now import modules under test ────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
import { v4 as uuid } from "uuid";
|
|
107
|
+
|
|
108
|
+
import { insertObservation } from "../memory/archive-store.js";
|
|
109
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
110
|
+
import { getSqlite } from "../memory/db-connection.js";
|
|
111
|
+
import { backfillSimplifiedMemoryJob } from "../memory/job-handlers/backfill-simplified-memory.js";
|
|
112
|
+
import type { MemoryJob } from "../memory/jobs-store.js";
|
|
113
|
+
import { conversations, memoryItems, messages } from "../memory/schema.js";
|
|
114
|
+
import {
|
|
115
|
+
handleMemoryRecall,
|
|
116
|
+
handleMemorySave,
|
|
117
|
+
} from "../tools/memory/handlers.js";
|
|
118
|
+
|
|
119
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function removeTestDbFiles(): void {
|
|
122
|
+
rmSync(dbPath, { force: true });
|
|
123
|
+
rmSync(`${dbPath}-shm`, { force: true });
|
|
124
|
+
rmSync(`${dbPath}-wal`, { force: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getRawDb(): import("bun:sqlite").Database {
|
|
128
|
+
return getSqlite();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function makeJob(overrides: Partial<MemoryJob> = {}): MemoryJob {
|
|
132
|
+
return {
|
|
133
|
+
id: uuid(),
|
|
134
|
+
type: "backfill_simplified_memory",
|
|
135
|
+
payload: {},
|
|
136
|
+
status: "running",
|
|
137
|
+
attempts: 0,
|
|
138
|
+
deferrals: 0,
|
|
139
|
+
runAfter: 0,
|
|
140
|
+
lastError: null,
|
|
141
|
+
startedAt: Date.now(),
|
|
142
|
+
createdAt: Date.now(),
|
|
143
|
+
updatedAt: Date.now(),
|
|
144
|
+
...overrides,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createConversation(id: string, title: string | null = null): void {
|
|
149
|
+
const db = getDb();
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
db.insert(conversations)
|
|
152
|
+
.values({
|
|
153
|
+
id,
|
|
154
|
+
title,
|
|
155
|
+
createdAt: now,
|
|
156
|
+
updatedAt: now,
|
|
157
|
+
})
|
|
158
|
+
.run();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let segmentIndexCounter = 0;
|
|
162
|
+
|
|
163
|
+
function insertLegacySegment(opts: {
|
|
164
|
+
id: string;
|
|
165
|
+
messageId: string;
|
|
166
|
+
conversationId: string;
|
|
167
|
+
role: string;
|
|
168
|
+
text: string;
|
|
169
|
+
scopeId?: string;
|
|
170
|
+
segmentIndex?: number;
|
|
171
|
+
}): void {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const idx = opts.segmentIndex ?? segmentIndexCounter++;
|
|
174
|
+
getRawDb().run(
|
|
175
|
+
`INSERT INTO memory_segments (id, message_id, conversation_id, role, segment_index, text, token_estimate, scope_id, created_at, updated_at)
|
|
176
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
177
|
+
[
|
|
178
|
+
opts.id,
|
|
179
|
+
opts.messageId,
|
|
180
|
+
opts.conversationId,
|
|
181
|
+
opts.role,
|
|
182
|
+
idx,
|
|
183
|
+
opts.text,
|
|
184
|
+
Math.ceil(opts.text.length / 4),
|
|
185
|
+
opts.scopeId ?? "default",
|
|
186
|
+
now,
|
|
187
|
+
now,
|
|
188
|
+
],
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function insertLegacySummary(opts: {
|
|
193
|
+
id: string;
|
|
194
|
+
scope: string;
|
|
195
|
+
scopeKey: string;
|
|
196
|
+
summary: string;
|
|
197
|
+
scopeId?: string;
|
|
198
|
+
startAt?: number;
|
|
199
|
+
endAt?: number;
|
|
200
|
+
}): void {
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
getRawDb().run(
|
|
203
|
+
`INSERT INTO memory_summaries (id, scope, scope_key, summary, token_estimate, version, scope_id, start_at, end_at, created_at, updated_at)
|
|
204
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)`,
|
|
205
|
+
[
|
|
206
|
+
opts.id,
|
|
207
|
+
opts.scope,
|
|
208
|
+
opts.scopeKey,
|
|
209
|
+
opts.summary,
|
|
210
|
+
Math.ceil(opts.summary.length / 4),
|
|
211
|
+
opts.scopeId ?? "default",
|
|
212
|
+
opts.startAt ?? now - 3600000,
|
|
213
|
+
opts.endAt ?? now,
|
|
214
|
+
now,
|
|
215
|
+
now,
|
|
216
|
+
],
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function insertLegacyItem(opts: {
|
|
221
|
+
id: string;
|
|
222
|
+
kind: string;
|
|
223
|
+
subject: string;
|
|
224
|
+
statement: string;
|
|
225
|
+
scopeId?: string;
|
|
226
|
+
confidence?: number;
|
|
227
|
+
status?: string;
|
|
228
|
+
validFrom?: number | null;
|
|
229
|
+
invalidAt?: number | null;
|
|
230
|
+
}): void {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
getRawDb().run(
|
|
233
|
+
`INSERT INTO memory_items (id, kind, subject, statement, status, confidence, importance, fingerprint, verification_state, scope_id, first_seen_at, last_seen_at)
|
|
234
|
+
VALUES (?, ?, ?, ?, ?, ?, 0.8, ?, 'assistant_inferred', ?, ?, ?)`,
|
|
235
|
+
[
|
|
236
|
+
opts.id,
|
|
237
|
+
opts.kind,
|
|
238
|
+
opts.subject,
|
|
239
|
+
opts.statement,
|
|
240
|
+
opts.status ?? "active",
|
|
241
|
+
opts.confidence ?? 0.9,
|
|
242
|
+
`fp-${opts.id}`,
|
|
243
|
+
opts.scopeId ?? "default",
|
|
244
|
+
now,
|
|
245
|
+
now,
|
|
246
|
+
],
|
|
247
|
+
);
|
|
248
|
+
// Set validFrom/invalidAt if provided
|
|
249
|
+
if (opts.validFrom != null || opts.invalidAt != null) {
|
|
250
|
+
getRawDb().run(
|
|
251
|
+
`UPDATE memory_items SET valid_from = ?, invalid_at = ? WHERE id = ?`,
|
|
252
|
+
[opts.validFrom ?? null, opts.invalidAt ?? null, opts.id],
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function insertMessage(
|
|
258
|
+
id: string,
|
|
259
|
+
conversationId: string,
|
|
260
|
+
role: string = "user",
|
|
261
|
+
content: string = "test message",
|
|
262
|
+
): void {
|
|
263
|
+
const db = getDb();
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
db.insert(messages)
|
|
266
|
+
.values({
|
|
267
|
+
id,
|
|
268
|
+
conversationId,
|
|
269
|
+
role,
|
|
270
|
+
content,
|
|
271
|
+
createdAt: now,
|
|
272
|
+
})
|
|
273
|
+
.run();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function countRows(table: string): number {
|
|
277
|
+
const result = getRawDb()
|
|
278
|
+
.query<{ c: number }, []>(`SELECT COUNT(*) as c FROM ${table}`)
|
|
279
|
+
.get();
|
|
280
|
+
return result?.c ?? 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Setup / Teardown ────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
describe("Simplified Memory E2E", () => {
|
|
286
|
+
beforeAll(() => {
|
|
287
|
+
initializeDb();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
beforeEach(() => {
|
|
291
|
+
segmentIndexCounter = 0;
|
|
292
|
+
resetDb();
|
|
293
|
+
removeTestDbFiles();
|
|
294
|
+
initializeDb();
|
|
295
|
+
testConfig = {
|
|
296
|
+
...DEFAULT_CONFIG,
|
|
297
|
+
memory: {
|
|
298
|
+
...DEFAULT_CONFIG.memory,
|
|
299
|
+
enabled: true,
|
|
300
|
+
simplified: {
|
|
301
|
+
...DEFAULT_CONFIG.memory.simplified,
|
|
302
|
+
enabled: true,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
afterAll(() => {
|
|
309
|
+
resetDb();
|
|
310
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── 1. Backfill tests ──────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
describe("backfill: legacy data migration", () => {
|
|
316
|
+
test("migrates legacy segments to observations and chunks", async () => {
|
|
317
|
+
const convId = uuid();
|
|
318
|
+
createConversation(convId);
|
|
319
|
+
const msgId = uuid();
|
|
320
|
+
insertMessage(msgId, convId, "user", "I like TypeScript");
|
|
321
|
+
|
|
322
|
+
insertLegacySegment({
|
|
323
|
+
id: "seg-1",
|
|
324
|
+
messageId: msgId,
|
|
325
|
+
conversationId: convId,
|
|
326
|
+
role: "user",
|
|
327
|
+
text: "I like TypeScript and prefer it over JavaScript",
|
|
328
|
+
});
|
|
329
|
+
insertLegacySegment({
|
|
330
|
+
id: "seg-2",
|
|
331
|
+
messageId: msgId,
|
|
332
|
+
conversationId: convId,
|
|
333
|
+
role: "user",
|
|
334
|
+
text: "My favorite editor is VS Code",
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(countRows("memory_segments")).toBe(2);
|
|
338
|
+
expect(countRows("memory_observations")).toBe(0);
|
|
339
|
+
|
|
340
|
+
await backfillSimplifiedMemoryJob(makeJob());
|
|
341
|
+
|
|
342
|
+
// Segments should have been migrated to observations
|
|
343
|
+
expect(countRows("memory_observations")).toBeGreaterThanOrEqual(2);
|
|
344
|
+
// Chunks should have been created for each observation
|
|
345
|
+
expect(countRows("memory_chunks")).toBeGreaterThanOrEqual(2);
|
|
346
|
+
// Original segments remain untouched
|
|
347
|
+
expect(countRows("memory_segments")).toBe(2);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("migrates legacy summaries to episodes", async () => {
|
|
351
|
+
const convId = uuid();
|
|
352
|
+
createConversation(convId);
|
|
353
|
+
|
|
354
|
+
insertLegacySummary({
|
|
355
|
+
id: "sum-1",
|
|
356
|
+
scope: "conversation",
|
|
357
|
+
scopeKey: convId,
|
|
358
|
+
summary: "User discussed TypeScript preferences and project setup",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(countRows("memory_summaries")).toBe(1);
|
|
362
|
+
expect(countRows("memory_episodes")).toBe(0);
|
|
363
|
+
|
|
364
|
+
await backfillSimplifiedMemoryJob(makeJob());
|
|
365
|
+
|
|
366
|
+
expect(countRows("memory_episodes")).toBeGreaterThanOrEqual(1);
|
|
367
|
+
// Original summaries remain untouched
|
|
368
|
+
expect(countRows("memory_summaries")).toBe(1);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("migrates active legacy items to observations", async () => {
|
|
372
|
+
insertLegacyItem({
|
|
373
|
+
id: "item-1",
|
|
374
|
+
kind: "preference",
|
|
375
|
+
subject: "Editor",
|
|
376
|
+
statement: "Prefers VS Code with vim keybindings",
|
|
377
|
+
});
|
|
378
|
+
insertLegacyItem({
|
|
379
|
+
id: "item-2",
|
|
380
|
+
kind: "identity",
|
|
381
|
+
subject: "Name",
|
|
382
|
+
statement: "User's name is Alice",
|
|
383
|
+
});
|
|
384
|
+
// Low-confidence item should be skipped
|
|
385
|
+
insertLegacyItem({
|
|
386
|
+
id: "item-3",
|
|
387
|
+
kind: "event",
|
|
388
|
+
subject: "Meeting",
|
|
389
|
+
statement: "Had a meeting yesterday",
|
|
390
|
+
confidence: 0.3,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
expect(countRows("memory_items")).toBe(3);
|
|
394
|
+
expect(countRows("memory_observations")).toBe(0);
|
|
395
|
+
|
|
396
|
+
await backfillSimplifiedMemoryJob(makeJob());
|
|
397
|
+
|
|
398
|
+
// Only the 2 active, high-confidence items should be migrated
|
|
399
|
+
expect(countRows("memory_observations")).toBeGreaterThanOrEqual(2);
|
|
400
|
+
// Original items remain untouched
|
|
401
|
+
expect(countRows("memory_items")).toBe(3);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("backfill is idempotent — running twice does not duplicate", async () => {
|
|
405
|
+
const convId = uuid();
|
|
406
|
+
createConversation(convId);
|
|
407
|
+
const msgId = uuid();
|
|
408
|
+
insertMessage(msgId, convId, "user", "Hello");
|
|
409
|
+
|
|
410
|
+
insertLegacySegment({
|
|
411
|
+
id: "seg-idem-1",
|
|
412
|
+
messageId: msgId,
|
|
413
|
+
conversationId: convId,
|
|
414
|
+
role: "user",
|
|
415
|
+
text: "This is an idempotency test segment",
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await backfillSimplifiedMemoryJob(makeJob());
|
|
419
|
+
const firstRunObservations = countRows("memory_observations");
|
|
420
|
+
|
|
421
|
+
// Run again — should not create duplicates because content-hash dedup
|
|
422
|
+
// and checkpoint tracking prevent it
|
|
423
|
+
await backfillSimplifiedMemoryJob(makeJob());
|
|
424
|
+
const secondRunObservations = countRows("memory_observations");
|
|
425
|
+
|
|
426
|
+
expect(secondRunObservations).toBe(firstRunObservations);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("backfill skips non-conversation summaries", async () => {
|
|
430
|
+
insertLegacySummary({
|
|
431
|
+
id: "sum-skip-1",
|
|
432
|
+
scope: "weekly",
|
|
433
|
+
scopeKey: "2024-W01",
|
|
434
|
+
summary: "A weekly summary that does not link to a conversation",
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await backfillSimplifiedMemoryJob(makeJob());
|
|
438
|
+
|
|
439
|
+
// Non-conversation summaries should be skipped (no episode created)
|
|
440
|
+
// The summary has scope "weekly" with a non-conversation-id scope_key
|
|
441
|
+
// so it should be skipped by the extractConversationId check.
|
|
442
|
+
// (Weekly summaries have no valid conversation to link to.)
|
|
443
|
+
expect(countRows("memory_episodes")).toBe(0);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ── 2. Default flag state ─────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
describe("simplified memory enabled by default", () => {
|
|
450
|
+
test("config defaults to simplified.enabled = true", () => {
|
|
451
|
+
expect(testConfig.memory.simplified.enabled).toBe(true);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("can be disabled for rollback via config override", () => {
|
|
455
|
+
testConfig = {
|
|
456
|
+
...testConfig,
|
|
457
|
+
memory: {
|
|
458
|
+
...testConfig.memory,
|
|
459
|
+
simplified: {
|
|
460
|
+
...testConfig.memory.simplified,
|
|
461
|
+
enabled: false,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
expect(testConfig.memory.simplified.enabled).toBe(false);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// ── 3. Memory tools use simplified path ────────────────────────────
|
|
470
|
+
|
|
471
|
+
describe("memory tools use simplified system when enabled", () => {
|
|
472
|
+
test("memory_save writes to observations when simplified is enabled", async () => {
|
|
473
|
+
const convId = uuid();
|
|
474
|
+
createConversation(convId);
|
|
475
|
+
|
|
476
|
+
const result = await handleMemorySave(
|
|
477
|
+
{
|
|
478
|
+
statement: "User prefers dark mode",
|
|
479
|
+
kind: "preference",
|
|
480
|
+
subject: "UI theme",
|
|
481
|
+
},
|
|
482
|
+
testConfig,
|
|
483
|
+
convId,
|
|
484
|
+
undefined,
|
|
485
|
+
"default",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
expect(result.isError).toBe(false);
|
|
489
|
+
expect(result.content).toContain("Saved to memory");
|
|
490
|
+
|
|
491
|
+
// Should have written to observations, not memory_items
|
|
492
|
+
expect(countRows("memory_observations")).toBeGreaterThanOrEqual(1);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("memory_save writes to memory_items when simplified is disabled", async () => {
|
|
496
|
+
testConfig = {
|
|
497
|
+
...testConfig,
|
|
498
|
+
memory: {
|
|
499
|
+
...testConfig.memory,
|
|
500
|
+
simplified: {
|
|
501
|
+
...testConfig.memory.simplified,
|
|
502
|
+
enabled: false,
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const convId = uuid();
|
|
508
|
+
createConversation(convId);
|
|
509
|
+
|
|
510
|
+
const result = await handleMemorySave(
|
|
511
|
+
{
|
|
512
|
+
statement: "User prefers light mode",
|
|
513
|
+
kind: "preference",
|
|
514
|
+
subject: "UI theme",
|
|
515
|
+
},
|
|
516
|
+
testConfig,
|
|
517
|
+
convId,
|
|
518
|
+
undefined,
|
|
519
|
+
"default",
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
expect(result.isError).toBe(false);
|
|
523
|
+
expect(result.content).toContain("Saved to memory");
|
|
524
|
+
|
|
525
|
+
// Should have written to legacy memory_items
|
|
526
|
+
expect(countRows("memory_items")).toBeGreaterThanOrEqual(1);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("memory_recall uses archive recall when simplified is enabled", async () => {
|
|
530
|
+
// Insert some archive data that the recall can find
|
|
531
|
+
const convId = uuid();
|
|
532
|
+
createConversation(convId);
|
|
533
|
+
|
|
534
|
+
insertObservation({
|
|
535
|
+
conversationId: convId,
|
|
536
|
+
role: "user",
|
|
537
|
+
content:
|
|
538
|
+
"User mentioned that their favorite programming language is TypeScript",
|
|
539
|
+
scopeId: "default",
|
|
540
|
+
modality: "text",
|
|
541
|
+
source: "test",
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const result = await handleMemoryRecall(
|
|
545
|
+
{ query: "programming language TypeScript" },
|
|
546
|
+
testConfig,
|
|
547
|
+
"default",
|
|
548
|
+
convId,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
expect(result.isError).toBe(false);
|
|
552
|
+
// The result should be valid JSON
|
|
553
|
+
const parsed = JSON.parse(result.content);
|
|
554
|
+
expect(parsed).toBeDefined();
|
|
555
|
+
expect(typeof parsed.text).toBe("string");
|
|
556
|
+
expect(typeof parsed.resultCount).toBe("number");
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// ── 4. Legacy tables remain available ──────────────────────────────
|
|
561
|
+
|
|
562
|
+
describe("legacy tables remain for rollback", () => {
|
|
563
|
+
test("legacy memory_items table still exists and is writable", () => {
|
|
564
|
+
const db = getDb();
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
|
|
567
|
+
// Write to the legacy table should succeed
|
|
568
|
+
db.insert(memoryItems)
|
|
569
|
+
.values({
|
|
570
|
+
id: uuid(),
|
|
571
|
+
kind: "preference",
|
|
572
|
+
subject: "Test",
|
|
573
|
+
statement: "Test statement",
|
|
574
|
+
status: "active",
|
|
575
|
+
confidence: 0.9,
|
|
576
|
+
importance: 0.8,
|
|
577
|
+
fingerprint: `fp-${uuid()}`,
|
|
578
|
+
verificationState: "assistant_inferred",
|
|
579
|
+
scopeId: "default",
|
|
580
|
+
firstSeenAt: now,
|
|
581
|
+
lastSeenAt: now,
|
|
582
|
+
})
|
|
583
|
+
.run();
|
|
584
|
+
|
|
585
|
+
expect(countRows("memory_items")).toBe(1);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("legacy memory_segments table still exists and is writable", () => {
|
|
589
|
+
const convId = uuid();
|
|
590
|
+
createConversation(convId);
|
|
591
|
+
const msgId = uuid();
|
|
592
|
+
insertMessage(msgId, convId, "user", "test");
|
|
593
|
+
|
|
594
|
+
insertLegacySegment({
|
|
595
|
+
id: uuid(),
|
|
596
|
+
messageId: msgId,
|
|
597
|
+
conversationId: convId,
|
|
598
|
+
role: "user",
|
|
599
|
+
text: "Test legacy segment",
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
expect(countRows("memory_segments")).toBe(1);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("legacy memory_summaries table still exists and is writable", () => {
|
|
606
|
+
const convId = uuid();
|
|
607
|
+
createConversation(convId);
|
|
608
|
+
|
|
609
|
+
insertLegacySummary({
|
|
610
|
+
id: uuid(),
|
|
611
|
+
scope: "conversation",
|
|
612
|
+
scopeKey: convId,
|
|
613
|
+
summary: "Test legacy summary",
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(countRows("memory_summaries")).toBe(1);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("switching to legacy mode by disabling simplified flag works", async () => {
|
|
620
|
+
// First save with simplified enabled
|
|
621
|
+
const convId = uuid();
|
|
622
|
+
createConversation(convId);
|
|
623
|
+
|
|
624
|
+
await handleMemorySave(
|
|
625
|
+
{
|
|
626
|
+
statement: "User likes coffee",
|
|
627
|
+
kind: "preference",
|
|
628
|
+
subject: "Beverage",
|
|
629
|
+
},
|
|
630
|
+
testConfig,
|
|
631
|
+
convId,
|
|
632
|
+
undefined,
|
|
633
|
+
"default",
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const simplifiedObs = countRows("memory_observations");
|
|
637
|
+
expect(simplifiedObs).toBeGreaterThanOrEqual(1);
|
|
638
|
+
|
|
639
|
+
// Now disable simplified and save — should go to legacy table
|
|
640
|
+
testConfig = {
|
|
641
|
+
...testConfig,
|
|
642
|
+
memory: {
|
|
643
|
+
...testConfig.memory,
|
|
644
|
+
simplified: {
|
|
645
|
+
...testConfig.memory.simplified,
|
|
646
|
+
enabled: false,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
await handleMemorySave(
|
|
652
|
+
{
|
|
653
|
+
statement: "User likes tea",
|
|
654
|
+
kind: "preference",
|
|
655
|
+
subject: "Beverage",
|
|
656
|
+
},
|
|
657
|
+
testConfig,
|
|
658
|
+
convId,
|
|
659
|
+
undefined,
|
|
660
|
+
"default",
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(countRows("memory_items")).toBeGreaterThanOrEqual(1);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
});
|