@vellumai/assistant 0.5.4 → 0.5.6
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 +17 -27
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +113 -0
- package/src/__tests__/config-schema.test.ts +2 -2
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-title-service.test.ts +30 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +0 -18
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/env.ts +8 -2
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +0 -12
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -0
- package/src/context/window-manager.ts +53 -2
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +10 -47
- package/src/daemon/providers-setup.ts +2 -1
- package/src/followups/followup-store.ts +5 -2
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +26 -10
- package/src/memory/db-init.ts +5 -13
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/indexer.ts +15 -106
- package/src/memory/job-handlers/conversation-starters.ts +24 -36
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +0 -8
- package/src/memory/jobs-worker.ts +0 -20
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/index.ts +1 -3
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +332 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +531 -39
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +4 -2
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +44 -2
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/secret-routes.ts +4 -1
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +21 -19
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/load.ts +9 -2
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- package/src/memory/schema/memory-brief.ts +0 -55
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the chunk dual-write path in the memory indexer.
|
|
3
|
-
*
|
|
4
|
-
* The indexer now writes both legacy memory_segments AND archive
|
|
5
|
-
* memory_chunks using the same segmentation boundaries. These tests
|
|
6
|
-
* verify:
|
|
7
|
-
*
|
|
8
|
-
* 1. Chunks are created alongside segments with matching boundaries.
|
|
9
|
-
* 2. Unchanged chunk content does not enqueue duplicate embed_chunk jobs.
|
|
10
|
-
* 3. Changed chunk content enqueues an embed_chunk job.
|
|
11
|
-
* 4. The legacy memory_segments path remains intact.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
18
|
-
|
|
19
|
-
import { eq } from "drizzle-orm";
|
|
20
|
-
|
|
21
|
-
const testDir = mkdtempSync(join(tmpdir(), "memory-chunk-dual-write-"));
|
|
22
|
-
|
|
23
|
-
mock.module("../util/platform.js", () => ({
|
|
24
|
-
getDataDir: () => testDir,
|
|
25
|
-
isMacOS: () => process.platform === "darwin",
|
|
26
|
-
isLinux: () => process.platform === "linux",
|
|
27
|
-
isWindows: () => process.platform === "win32",
|
|
28
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
29
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
30
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
31
|
-
ensureDataDir: () => {},
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
mock.module("../util/logger.js", () => ({
|
|
35
|
-
getLogger: () =>
|
|
36
|
-
new Proxy({} as Record<string, unknown>, {
|
|
37
|
-
get: () => () => {},
|
|
38
|
-
}),
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
mock.module("../memory/qdrant-client.js", () => ({
|
|
42
|
-
getQdrantClient: () => ({
|
|
43
|
-
searchWithFilter: async () => [],
|
|
44
|
-
hybridSearch: async () => [],
|
|
45
|
-
upsertPoints: async () => {},
|
|
46
|
-
deletePoints: async () => {},
|
|
47
|
-
}),
|
|
48
|
-
initQdrantClient: () => {},
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
52
|
-
|
|
53
|
-
const TEST_CONFIG = {
|
|
54
|
-
...DEFAULT_CONFIG,
|
|
55
|
-
memory: {
|
|
56
|
-
...DEFAULT_CONFIG.memory,
|
|
57
|
-
enabled: true,
|
|
58
|
-
extraction: {
|
|
59
|
-
...DEFAULT_CONFIG.memory.extraction,
|
|
60
|
-
useLLM: false,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
mock.module("../config/loader.js", () => ({
|
|
66
|
-
loadConfig: () => TEST_CONFIG,
|
|
67
|
-
getConfig: () => TEST_CONFIG,
|
|
68
|
-
loadRawConfig: () => ({}),
|
|
69
|
-
saveRawConfig: () => {},
|
|
70
|
-
invalidateConfigCache: () => {},
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
import { getDb, initializeDb, resetDb, resetTestTables } from "../memory/db.js";
|
|
74
|
-
import { indexMessageNow } from "../memory/indexer.js";
|
|
75
|
-
import {
|
|
76
|
-
conversations,
|
|
77
|
-
memoryChunks,
|
|
78
|
-
memoryJobs,
|
|
79
|
-
memoryObservations,
|
|
80
|
-
memorySegments,
|
|
81
|
-
messages,
|
|
82
|
-
} from "../memory/schema.js";
|
|
83
|
-
|
|
84
|
-
// Initialize DB once for the entire file. Each test cleans its own tables.
|
|
85
|
-
initializeDb();
|
|
86
|
-
|
|
87
|
-
afterAll(() => {
|
|
88
|
-
resetDb();
|
|
89
|
-
try {
|
|
90
|
-
rmSync(testDir, { recursive: true });
|
|
91
|
-
} catch {
|
|
92
|
-
// best effort cleanup
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
function resetTables() {
|
|
97
|
-
resetTestTables(
|
|
98
|
-
"memory_chunks",
|
|
99
|
-
"memory_observations",
|
|
100
|
-
"memory_segments",
|
|
101
|
-
"memory_jobs",
|
|
102
|
-
"messages",
|
|
103
|
-
"conversations",
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Insert a minimal conversation + message row for FK references. */
|
|
108
|
-
function seedConversationAndMessage(
|
|
109
|
-
conversationId: string,
|
|
110
|
-
messageId: string,
|
|
111
|
-
text: string,
|
|
112
|
-
): void {
|
|
113
|
-
const db = getDb();
|
|
114
|
-
const now = Date.now();
|
|
115
|
-
db.insert(conversations)
|
|
116
|
-
.values({
|
|
117
|
-
id: conversationId,
|
|
118
|
-
title: null,
|
|
119
|
-
createdAt: now,
|
|
120
|
-
updatedAt: now,
|
|
121
|
-
})
|
|
122
|
-
.onConflictDoNothing()
|
|
123
|
-
.run();
|
|
124
|
-
|
|
125
|
-
db.insert(messages)
|
|
126
|
-
.values({
|
|
127
|
-
id: messageId,
|
|
128
|
-
conversationId,
|
|
129
|
-
role: "user",
|
|
130
|
-
content: JSON.stringify([{ type: "text", text }]),
|
|
131
|
-
createdAt: now,
|
|
132
|
-
})
|
|
133
|
-
.onConflictDoNothing()
|
|
134
|
-
.run();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
138
|
-
// Test suite: chunk dual-write alongside legacy segments
|
|
139
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
describe("chunk dual-write from the memory indexer", () => {
|
|
142
|
-
beforeEach(() => {
|
|
143
|
-
resetTables();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("indexing a message creates chunks alongside segments with matching boundaries", async () => {
|
|
147
|
-
const conversationId = "conv-dual-write-basic";
|
|
148
|
-
const messageId = "msg-dual-write-basic";
|
|
149
|
-
const text =
|
|
150
|
-
"I prefer TypeScript for large projects and always use strict mode.";
|
|
151
|
-
|
|
152
|
-
seedConversationAndMessage(conversationId, messageId, text);
|
|
153
|
-
|
|
154
|
-
const config = TEST_CONFIG.memory;
|
|
155
|
-
const result = await indexMessageNow(
|
|
156
|
-
{
|
|
157
|
-
messageId,
|
|
158
|
-
conversationId,
|
|
159
|
-
role: "user",
|
|
160
|
-
content: JSON.stringify([{ type: "text", text }]),
|
|
161
|
-
createdAt: Date.now(),
|
|
162
|
-
},
|
|
163
|
-
config,
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
expect(result.indexedSegments).toBeGreaterThanOrEqual(1);
|
|
167
|
-
|
|
168
|
-
const db = getDb();
|
|
169
|
-
|
|
170
|
-
// Verify segments were created (legacy path)
|
|
171
|
-
const segments = db
|
|
172
|
-
.select()
|
|
173
|
-
.from(memorySegments)
|
|
174
|
-
.where(eq(memorySegments.messageId, messageId))
|
|
175
|
-
.all();
|
|
176
|
-
expect(segments.length).toBeGreaterThanOrEqual(1);
|
|
177
|
-
|
|
178
|
-
// Verify chunks were created (dual-write path)
|
|
179
|
-
const observationId = `obs:${messageId}`;
|
|
180
|
-
const chunks = db
|
|
181
|
-
.select()
|
|
182
|
-
.from(memoryChunks)
|
|
183
|
-
.where(eq(memoryChunks.observationId, observationId))
|
|
184
|
-
.all();
|
|
185
|
-
expect(chunks.length).toBe(segments.length);
|
|
186
|
-
|
|
187
|
-
// Verify the observation was created
|
|
188
|
-
const observation = db
|
|
189
|
-
.select()
|
|
190
|
-
.from(memoryObservations)
|
|
191
|
-
.where(eq(memoryObservations.id, observationId))
|
|
192
|
-
.get();
|
|
193
|
-
expect(observation).toBeDefined();
|
|
194
|
-
expect(observation!.conversationId).toBe(conversationId);
|
|
195
|
-
expect(observation!.messageId).toBe(messageId);
|
|
196
|
-
expect(observation!.role).toBe("user");
|
|
197
|
-
|
|
198
|
-
// Verify chunk content matches segment content (same boundaries)
|
|
199
|
-
for (let i = 0; i < segments.length; i++) {
|
|
200
|
-
const chunkId = `chunk:${messageId}:${i}`;
|
|
201
|
-
const chunk = chunks.find((c) => c.id === chunkId);
|
|
202
|
-
expect(chunk).toBeDefined();
|
|
203
|
-
expect(chunk!.content).toBe(segments[i].text);
|
|
204
|
-
expect(chunk!.tokenEstimate).toBe(segments[i].tokenEstimate);
|
|
205
|
-
expect(chunk!.scopeId).toBe(segments[i].scopeId);
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("unchanged chunk content does not enqueue duplicate embed_chunk jobs", async () => {
|
|
210
|
-
const conversationId = "conv-unchanged-chunk";
|
|
211
|
-
const messageId = "msg-unchanged-chunk";
|
|
212
|
-
const text =
|
|
213
|
-
"My preferred timezone is America/Los_Angeles and I work remotely.";
|
|
214
|
-
|
|
215
|
-
seedConversationAndMessage(conversationId, messageId, text);
|
|
216
|
-
|
|
217
|
-
const config = TEST_CONFIG.memory;
|
|
218
|
-
const content = JSON.stringify([{ type: "text", text }]);
|
|
219
|
-
|
|
220
|
-
// First indexing — should enqueue embed_chunk jobs
|
|
221
|
-
await indexMessageNow(
|
|
222
|
-
{
|
|
223
|
-
messageId,
|
|
224
|
-
conversationId,
|
|
225
|
-
role: "user",
|
|
226
|
-
content,
|
|
227
|
-
createdAt: Date.now(),
|
|
228
|
-
},
|
|
229
|
-
config,
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
const db = getDb();
|
|
233
|
-
const jobsAfterFirst = db
|
|
234
|
-
.select()
|
|
235
|
-
.from(memoryJobs)
|
|
236
|
-
.where(eq(memoryJobs.type, "embed_chunk"))
|
|
237
|
-
.all();
|
|
238
|
-
const firstChunkJobCount = jobsAfterFirst.length;
|
|
239
|
-
expect(firstChunkJobCount).toBeGreaterThanOrEqual(1);
|
|
240
|
-
|
|
241
|
-
// Second indexing with identical content — should NOT enqueue more embed_chunk jobs
|
|
242
|
-
await indexMessageNow(
|
|
243
|
-
{
|
|
244
|
-
messageId,
|
|
245
|
-
conversationId,
|
|
246
|
-
role: "user",
|
|
247
|
-
content,
|
|
248
|
-
createdAt: Date.now(),
|
|
249
|
-
},
|
|
250
|
-
config,
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
const jobsAfterSecond = db
|
|
254
|
-
.select()
|
|
255
|
-
.from(memoryJobs)
|
|
256
|
-
.where(eq(memoryJobs.type, "embed_chunk"))
|
|
257
|
-
.all();
|
|
258
|
-
|
|
259
|
-
// No new embed_chunk jobs should have been enqueued
|
|
260
|
-
expect(jobsAfterSecond.length).toBe(firstChunkJobCount);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("changed chunk content enqueues new embed_chunk jobs", async () => {
|
|
264
|
-
const conversationId = "conv-changed-chunk";
|
|
265
|
-
const messageId = "msg-changed-chunk";
|
|
266
|
-
const textV1 = "I prefer React for frontend development work.";
|
|
267
|
-
const textV2 =
|
|
268
|
-
"I prefer Vue for frontend development work on large projects instead.";
|
|
269
|
-
|
|
270
|
-
seedConversationAndMessage(conversationId, messageId, textV1);
|
|
271
|
-
|
|
272
|
-
const config = TEST_CONFIG.memory;
|
|
273
|
-
|
|
274
|
-
// First indexing
|
|
275
|
-
await indexMessageNow(
|
|
276
|
-
{
|
|
277
|
-
messageId,
|
|
278
|
-
conversationId,
|
|
279
|
-
role: "user",
|
|
280
|
-
content: JSON.stringify([{ type: "text", text: textV1 }]),
|
|
281
|
-
createdAt: Date.now(),
|
|
282
|
-
},
|
|
283
|
-
config,
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
const db = getDb();
|
|
287
|
-
const jobsAfterFirst = db
|
|
288
|
-
.select()
|
|
289
|
-
.from(memoryJobs)
|
|
290
|
-
.where(eq(memoryJobs.type, "embed_chunk"))
|
|
291
|
-
.all();
|
|
292
|
-
const firstChunkJobCount = jobsAfterFirst.length;
|
|
293
|
-
expect(firstChunkJobCount).toBeGreaterThanOrEqual(1);
|
|
294
|
-
|
|
295
|
-
// Second indexing with DIFFERENT content — should enqueue new embed_chunk jobs
|
|
296
|
-
await indexMessageNow(
|
|
297
|
-
{
|
|
298
|
-
messageId,
|
|
299
|
-
conversationId,
|
|
300
|
-
role: "user",
|
|
301
|
-
content: JSON.stringify([{ type: "text", text: textV2 }]),
|
|
302
|
-
createdAt: Date.now(),
|
|
303
|
-
},
|
|
304
|
-
config,
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
const jobsAfterSecond = db
|
|
308
|
-
.select()
|
|
309
|
-
.from(memoryJobs)
|
|
310
|
-
.where(eq(memoryJobs.type, "embed_chunk"))
|
|
311
|
-
.all();
|
|
312
|
-
|
|
313
|
-
// New embed_chunk jobs should have been enqueued for the changed content
|
|
314
|
-
expect(jobsAfterSecond.length).toBeGreaterThan(firstChunkJobCount);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("legacy memory_segments path remains intact", async () => {
|
|
318
|
-
const conversationId = "conv-legacy-compat";
|
|
319
|
-
const messageId = "msg-legacy-compat";
|
|
320
|
-
const text =
|
|
321
|
-
"I always prefer concise code reviews and I work in a distributed team.";
|
|
322
|
-
|
|
323
|
-
seedConversationAndMessage(conversationId, messageId, text);
|
|
324
|
-
|
|
325
|
-
const config = TEST_CONFIG.memory;
|
|
326
|
-
const result = await indexMessageNow(
|
|
327
|
-
{
|
|
328
|
-
messageId,
|
|
329
|
-
conversationId,
|
|
330
|
-
role: "user",
|
|
331
|
-
content: JSON.stringify([{ type: "text", text }]),
|
|
332
|
-
createdAt: Date.now(),
|
|
333
|
-
},
|
|
334
|
-
config,
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
const db = getDb();
|
|
338
|
-
|
|
339
|
-
// Legacy segments must be present and correctly formed
|
|
340
|
-
const segments = db
|
|
341
|
-
.select()
|
|
342
|
-
.from(memorySegments)
|
|
343
|
-
.where(eq(memorySegments.messageId, messageId))
|
|
344
|
-
.all();
|
|
345
|
-
expect(segments.length).toBe(result.indexedSegments);
|
|
346
|
-
|
|
347
|
-
for (const seg of segments) {
|
|
348
|
-
expect(seg.id.startsWith(messageId + ":")).toBe(true);
|
|
349
|
-
expect(seg.conversationId).toBe(conversationId);
|
|
350
|
-
expect(seg.role).toBe("user");
|
|
351
|
-
expect(seg.text.length).toBeGreaterThan(0);
|
|
352
|
-
expect(seg.contentHash).toBeTruthy();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Legacy embed_segment jobs must be enqueued
|
|
356
|
-
const segmentJobs = db
|
|
357
|
-
.select()
|
|
358
|
-
.from(memoryJobs)
|
|
359
|
-
.where(eq(memoryJobs.type, "embed_segment"))
|
|
360
|
-
.all();
|
|
361
|
-
expect(segmentJobs.length).toBeGreaterThanOrEqual(1);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("repeated indexing produces exactly one chunk per segment boundary", async () => {
|
|
365
|
-
const conversationId = "conv-chunk-dedup";
|
|
366
|
-
const messageId = "msg-chunk-dedup";
|
|
367
|
-
const text =
|
|
368
|
-
"I prefer TypeScript over plain JavaScript for large projects.";
|
|
369
|
-
|
|
370
|
-
seedConversationAndMessage(conversationId, messageId, text);
|
|
371
|
-
|
|
372
|
-
const config = TEST_CONFIG.memory;
|
|
373
|
-
const content = JSON.stringify([{ type: "text", text }]);
|
|
374
|
-
|
|
375
|
-
// Index the same message multiple times
|
|
376
|
-
for (let i = 0; i < 5; i++) {
|
|
377
|
-
await indexMessageNow(
|
|
378
|
-
{
|
|
379
|
-
messageId,
|
|
380
|
-
conversationId,
|
|
381
|
-
role: "user",
|
|
382
|
-
content,
|
|
383
|
-
createdAt: Date.now(),
|
|
384
|
-
},
|
|
385
|
-
config,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const db = getDb();
|
|
390
|
-
const observationId = `obs:${messageId}`;
|
|
391
|
-
|
|
392
|
-
// Verify no duplicate chunks — one chunk per segment boundary
|
|
393
|
-
const chunks = db
|
|
394
|
-
.select()
|
|
395
|
-
.from(memoryChunks)
|
|
396
|
-
.where(eq(memoryChunks.observationId, observationId))
|
|
397
|
-
.all();
|
|
398
|
-
const segments = db
|
|
399
|
-
.select()
|
|
400
|
-
.from(memorySegments)
|
|
401
|
-
.where(eq(memorySegments.messageId, messageId))
|
|
402
|
-
.all();
|
|
403
|
-
|
|
404
|
-
expect(chunks.length).toBe(segments.length);
|
|
405
|
-
|
|
406
|
-
// Verify chunk IDs are unique
|
|
407
|
-
const chunkIds = chunks.map((c) => c.id);
|
|
408
|
-
const uniqueChunkIds = new Set(chunkIds);
|
|
409
|
-
expect(uniqueChunkIds.size).toBe(chunkIds.length);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("chunk dual-write respects custom scopeId", async () => {
|
|
413
|
-
const conversationId = "conv-scope";
|
|
414
|
-
const messageId = "msg-scope";
|
|
415
|
-
const text = "Custom scoped message content.";
|
|
416
|
-
const scopeId = "custom-scope-42";
|
|
417
|
-
|
|
418
|
-
seedConversationAndMessage(conversationId, messageId, text);
|
|
419
|
-
|
|
420
|
-
const config = TEST_CONFIG.memory;
|
|
421
|
-
await indexMessageNow(
|
|
422
|
-
{
|
|
423
|
-
messageId,
|
|
424
|
-
conversationId,
|
|
425
|
-
role: "user",
|
|
426
|
-
content: JSON.stringify([{ type: "text", text }]),
|
|
427
|
-
createdAt: Date.now(),
|
|
428
|
-
scopeId,
|
|
429
|
-
},
|
|
430
|
-
config,
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
const db = getDb();
|
|
434
|
-
const observationId = `obs:${messageId}`;
|
|
435
|
-
|
|
436
|
-
const observation = db
|
|
437
|
-
.select()
|
|
438
|
-
.from(memoryObservations)
|
|
439
|
-
.where(eq(memoryObservations.id, observationId))
|
|
440
|
-
.get();
|
|
441
|
-
expect(observation!.scopeId).toBe(scopeId);
|
|
442
|
-
|
|
443
|
-
const chunks = db
|
|
444
|
-
.select()
|
|
445
|
-
.from(memoryChunks)
|
|
446
|
-
.where(eq(memoryChunks.observationId, observationId))
|
|
447
|
-
.all();
|
|
448
|
-
expect(chunks.length).toBeGreaterThanOrEqual(1);
|
|
449
|
-
for (const chunk of chunks) {
|
|
450
|
-
expect(chunk.scopeId).toBe(scopeId);
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
});
|