@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.
Files changed (151) hide show
  1. package/Dockerfile +17 -27
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
  3. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/actor-token-service.test.ts +113 -0
  6. package/src/__tests__/config-schema.test.ts +2 -2
  7. package/src/__tests__/context-window-manager.test.ts +78 -0
  8. package/src/__tests__/conversation-title-service.test.ts +30 -1
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  10. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  11. package/src/__tests__/memory-regressions.test.ts +8 -30
  12. package/src/__tests__/openai-whisper.test.ts +93 -0
  13. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  14. package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
  15. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  16. package/src/__tests__/tool-executor.test.ts +4 -0
  17. package/src/__tests__/volume-security-guard.test.ts +155 -0
  18. package/src/cli/commands/conversations.ts +0 -18
  19. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
  20. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
  21. package/src/config/env-registry.ts +9 -0
  22. package/src/config/env.ts +8 -2
  23. package/src/config/feature-flag-registry.json +8 -8
  24. package/src/config/schema.ts +0 -12
  25. package/src/config/schemas/memory.ts +0 -4
  26. package/src/config/schemas/platform.ts +1 -1
  27. package/src/config/schemas/security.ts +4 -0
  28. package/src/context/window-manager.ts +53 -2
  29. package/src/credential-execution/managed-catalog.ts +5 -15
  30. package/src/daemon/conversation-agent-loop.ts +0 -60
  31. package/src/daemon/conversation-memory.ts +0 -117
  32. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  33. package/src/daemon/daemon-control.ts +7 -0
  34. package/src/daemon/handlers/conversations.ts +0 -11
  35. package/src/daemon/lifecycle.ts +10 -47
  36. package/src/daemon/providers-setup.ts +2 -1
  37. package/src/followups/followup-store.ts +5 -2
  38. package/src/hooks/manager.ts +7 -0
  39. package/src/instrument.ts +33 -1
  40. package/src/memory/conversation-crud.ts +0 -236
  41. package/src/memory/conversation-title-service.ts +26 -10
  42. package/src/memory/db-init.ts +5 -13
  43. package/src/memory/embedding-local.ts +11 -5
  44. package/src/memory/indexer.ts +15 -106
  45. package/src/memory/job-handlers/conversation-starters.ts +24 -36
  46. package/src/memory/job-handlers/embedding.ts +0 -79
  47. package/src/memory/job-utils.ts +1 -1
  48. package/src/memory/jobs-store.ts +0 -8
  49. package/src/memory/jobs-worker.ts +0 -20
  50. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  51. package/src/memory/migrations/index.ts +1 -3
  52. package/src/memory/qdrant-client.ts +4 -6
  53. package/src/memory/schema/conversations.ts +0 -3
  54. package/src/memory/schema/index.ts +0 -2
  55. package/src/messaging/draft-store.ts +2 -2
  56. package/src/messaging/provider.ts +9 -0
  57. package/src/messaging/providers/slack/adapter.ts +29 -2
  58. package/src/oauth/connection-resolver.test.ts +22 -18
  59. package/src/oauth/connection-resolver.ts +92 -7
  60. package/src/oauth/platform-connection.test.ts +78 -69
  61. package/src/oauth/platform-connection.ts +12 -19
  62. package/src/permissions/defaults.ts +3 -3
  63. package/src/permissions/trust-client.ts +332 -0
  64. package/src/permissions/trust-store-interface.ts +105 -0
  65. package/src/permissions/trust-store.ts +531 -39
  66. package/src/platform/client.test.ts +148 -0
  67. package/src/platform/client.ts +71 -0
  68. package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
  69. package/src/providers/speech-to-text/openai-whisper.ts +68 -0
  70. package/src/providers/speech-to-text/resolve.ts +9 -0
  71. package/src/providers/speech-to-text/types.ts +17 -0
  72. package/src/runtime/auth/route-policy.ts +14 -0
  73. package/src/runtime/auth/token-service.ts +133 -0
  74. package/src/runtime/http-server.ts +4 -2
  75. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  76. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  77. package/src/runtime/routes/conversation-routes.ts +2 -1
  78. package/src/runtime/routes/inbound-message-handler.ts +27 -3
  79. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
  80. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
  81. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
  82. package/src/runtime/routes/log-export-routes.ts +1 -0
  83. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  84. package/src/runtime/routes/memory-item-routes.ts +124 -2
  85. package/src/runtime/routes/secret-routes.ts +4 -1
  86. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  87. package/src/schedule/schedule-store.ts +0 -21
  88. package/src/security/ces-credential-client.ts +173 -0
  89. package/src/security/secure-keys.ts +65 -22
  90. package/src/signals/bash.ts +3 -0
  91. package/src/signals/cancel.ts +3 -0
  92. package/src/signals/confirm.ts +3 -0
  93. package/src/signals/conversation-undo.ts +3 -0
  94. package/src/signals/event-stream.ts +7 -0
  95. package/src/signals/shotgun.ts +3 -0
  96. package/src/signals/trust-rule.ts +3 -0
  97. package/src/skills/inline-command-render.ts +5 -1
  98. package/src/skills/inline-command-runner.ts +30 -2
  99. package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
  100. package/src/telemetry/usage-telemetry-reporter.ts +21 -19
  101. package/src/tools/memory/handlers.ts +1 -129
  102. package/src/tools/permission-checker.ts +18 -0
  103. package/src/tools/skills/load.ts +9 -2
  104. package/src/util/device-id.ts +70 -7
  105. package/src/util/logger.ts +35 -9
  106. package/src/util/platform.ts +29 -5
  107. package/src/util/xml.ts +8 -0
  108. package/src/workspace/heartbeat-service.ts +5 -24
  109. package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
  110. package/src/workspace/migrations/registry.ts +2 -0
  111. package/src/__tests__/archive-recall.test.ts +0 -560
  112. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  113. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  114. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  115. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  116. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  117. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  118. package/src/__tests__/memory-brief-time.test.ts +0 -285
  119. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  120. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  121. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  122. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  123. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  124. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  125. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  126. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  127. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  128. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  129. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  130. package/src/__tests__/memory-reducer.test.ts +0 -704
  131. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  132. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  133. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  134. package/src/config/schemas/memory-simplified.ts +0 -101
  135. package/src/memory/archive-recall.ts +0 -516
  136. package/src/memory/archive-store.ts +0 -400
  137. package/src/memory/brief-formatting.ts +0 -33
  138. package/src/memory/brief-open-loops.ts +0 -266
  139. package/src/memory/brief-time.ts +0 -162
  140. package/src/memory/brief.ts +0 -75
  141. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  142. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  143. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  144. package/src/memory/migrations/186-memory-archive.ts +0 -109
  145. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  146. package/src/memory/reducer-scheduler.ts +0 -242
  147. package/src/memory/reducer-store.ts +0 -271
  148. package/src/memory/reducer-types.ts +0 -106
  149. package/src/memory/reducer.ts +0 -467
  150. package/src/memory/schema/memory-archive.ts +0 -121
  151. package/src/memory/schema/memory-brief.ts +0 -55
@@ -1,616 +0,0 @@
1
- /**
2
- * Tests for the simplified memory runtime injection path in
3
- * conversation-memory.ts.
4
- *
5
- * Covers:
6
- * - Brief-only turns (no archive recall trigger)
7
- * - Brief-plus-recall turns (archive recall fires)
8
- * - Disabled-flag fallback (legacy path used when flag is off)
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-runtime-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
- // ── Configurable config mock ────────────────────────────────────────
49
-
50
- import { DEFAULT_CONFIG } from "../config/defaults.js";
51
- import type { AssistantConfig } from "../config/types.js";
52
-
53
- let testConfig: AssistantConfig = {
54
- ...DEFAULT_CONFIG,
55
- memory: {
56
- ...DEFAULT_CONFIG.memory,
57
- enabled: true,
58
- simplified: {
59
- ...DEFAULT_CONFIG.memory.simplified,
60
- enabled: true,
61
- },
62
- },
63
- };
64
-
65
- mock.module("../config/loader.js", () => ({
66
- loadConfig: () => testConfig,
67
- getConfig: () => testConfig,
68
- loadRawConfig: () => ({}),
69
- saveRawConfig: () => {},
70
- invalidateConfigCache: () => {},
71
- }));
72
-
73
- // Stub out the legacy retriever to ensure the simplified path does not
74
- // call into the heavy V2 hybrid pipeline. If the legacy path is used
75
- // unexpectedly, the test will fail with a clear error.
76
- //
77
- // We provide the real `injectMemoryRecallAsUserBlock` inline since
78
- // it's a pure function used by both the legacy and simplified paths.
79
- mock.module("../memory/retriever.js", () => ({
80
- buildMemoryRecall: () => {
81
- throw new Error(
82
- "buildMemoryRecall should not be called in simplified mode",
83
- );
84
- },
85
- injectMemoryRecallAsUserBlock: (
86
- msgs: import("../providers/types.js").Message[],
87
- memoryRecallText: string,
88
- ): import("../providers/types.js").Message[] => {
89
- if (memoryRecallText.trim().length === 0) return msgs;
90
- if (msgs.length === 0) return msgs;
91
- const userTail = msgs[msgs.length - 1];
92
- if (!userTail || userTail.role !== "user") return msgs;
93
- return [
94
- ...msgs.slice(0, -1),
95
- {
96
- ...userTail,
97
- content: [
98
- { type: "text" as const, text: memoryRecallText },
99
- ...userTail.content,
100
- ],
101
- },
102
- ];
103
- },
104
- }));
105
-
106
- // Stub out modules used only by the legacy pipeline (budget, token
107
- // estimator, query builder) so they never execute in simplified mode.
108
- mock.module("../memory/query-builder.js", () => ({
109
- buildMemoryQuery: () => {
110
- throw new Error("buildMemoryQuery should not be called in simplified mode");
111
- },
112
- }));
113
- mock.module("../memory/retrieval-budget.js", () => ({
114
- computeRecallBudget: () => {
115
- throw new Error(
116
- "computeRecallBudget should not be called in simplified mode",
117
- );
118
- },
119
- }));
120
- mock.module("../context/token-estimator.js", () => ({
121
- estimatePromptTokens: () => 0,
122
- }));
123
-
124
- // ── Now import the module under test ────────────────────────────────
125
-
126
- import { v4 as uuid } from "uuid";
127
-
128
- import {
129
- type MemoryPrepareContext,
130
- prepareMemoryContext,
131
- } from "../daemon/conversation-memory.js";
132
- import type { ServerMessage } from "../daemon/message-protocol.js";
133
- import {
134
- insertCompactionEpisode,
135
- insertObservation,
136
- } from "../memory/archive-store.js";
137
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
138
- import { getSqlite } from "../memory/db-connection.js";
139
- import { conversations, messages } from "../memory/schema.js";
140
- import type { Message } from "../providers/types.js";
141
-
142
- // ── Helpers ─────────────────────────────────────────────────────────
143
-
144
- function removeTestDbFiles(): void {
145
- rmSync(dbPath, { force: true });
146
- rmSync(`${dbPath}-shm`, { force: true });
147
- rmSync(`${dbPath}-wal`, { force: true });
148
- }
149
-
150
- function getRawDb(): import("bun:sqlite").Database {
151
- return getSqlite();
152
- }
153
-
154
- function insertTimeContext(opts: {
155
- id: string;
156
- summary: string;
157
- activeFrom: number;
158
- activeUntil: number;
159
- scopeId?: string;
160
- }): void {
161
- const now = Date.now();
162
- getRawDb().run(
163
- `INSERT INTO time_contexts (id, scope_id, summary, source, active_from, active_until, created_at, updated_at)
164
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
165
- [
166
- opts.id,
167
- opts.scopeId ?? "default",
168
- opts.summary,
169
- "conversation",
170
- opts.activeFrom,
171
- opts.activeUntil,
172
- now,
173
- now,
174
- ],
175
- );
176
- }
177
-
178
- function insertOpenLoop(opts: {
179
- id: string;
180
- summary: string;
181
- dueAt?: number | null;
182
- updatedAt?: number;
183
- }): void {
184
- const now = Date.now();
185
- getRawDb().run(
186
- `INSERT INTO open_loops (id, scope_id, summary, status, source, due_at, surfaced_at, created_at, updated_at)
187
- VALUES (?, ?, ?, ?, 'conversation', ?, ?, ?, ?)`,
188
- [
189
- opts.id,
190
- "default",
191
- opts.summary,
192
- "open",
193
- opts.dueAt ?? null,
194
- null,
195
- now,
196
- opts.updatedAt ?? now,
197
- ],
198
- );
199
- }
200
-
201
- function createConversation(id: string, title: string | null = null): void {
202
- const db = getDb();
203
- const now = Date.now();
204
- db.insert(conversations)
205
- .values({
206
- id,
207
- title,
208
- createdAt: now,
209
- updatedAt: now,
210
- })
211
- .run();
212
- }
213
-
214
- function createMessage(
215
- id: string,
216
- conversationId: string,
217
- role: string = "user",
218
- content: string = "test message",
219
- ): void {
220
- const db = getDb();
221
- db.insert(messages)
222
- .values({
223
- id,
224
- conversationId,
225
- role,
226
- content,
227
- createdAt: Date.now(),
228
- })
229
- .run();
230
- }
231
-
232
- function makeUserMessage(text: string): Message {
233
- return {
234
- role: "user",
235
- content: [{ type: "text", text }],
236
- };
237
- }
238
-
239
- function buildCtx(
240
- overrides: Partial<MemoryPrepareContext> = {},
241
- ): MemoryPrepareContext {
242
- return {
243
- conversationId: uuid(),
244
- messages: [makeUserMessage("Hello")],
245
- systemPrompt: "",
246
- provider: { name: "anthropic" } as MemoryPrepareContext["provider"],
247
- scopeId: "default",
248
- includeDefaultFallback: true,
249
- trustClass: "guardian",
250
- ...overrides,
251
- };
252
- }
253
-
254
- const HOUR = 60 * 60 * 1000;
255
- const DAY = 24 * HOUR;
256
-
257
- // ── Setup / Teardown ────────────────────────────────────────────────
258
-
259
- describe("Simplified Memory Runtime", () => {
260
- const events: ServerMessage[] = [];
261
- const onEvent = (msg: ServerMessage) => events.push(msg);
262
- const abortController = new AbortController();
263
-
264
- beforeAll(() => {
265
- initializeDb();
266
- });
267
-
268
- beforeEach(() => {
269
- events.length = 0;
270
- resetDb();
271
- removeTestDbFiles();
272
- initializeDb();
273
- // Reset config to simplified-enabled for each test
274
- testConfig = {
275
- ...DEFAULT_CONFIG,
276
- memory: {
277
- ...DEFAULT_CONFIG.memory,
278
- enabled: true,
279
- simplified: {
280
- ...DEFAULT_CONFIG.memory.simplified,
281
- enabled: true,
282
- },
283
- },
284
- };
285
- });
286
-
287
- afterAll(() => {
288
- resetDb();
289
- rmSync(testDir, { recursive: true, force: true });
290
- });
291
-
292
- // ── Brief-only turns ────────────────────────────────────────────
293
-
294
- describe("brief-only turns", () => {
295
- test("injects <memory_brief> when time context exists", async () => {
296
- const now = Date.now();
297
- insertTimeContext({
298
- id: "tc-1",
299
- summary: "Deploy to staging at 3pm",
300
- activeFrom: now - HOUR,
301
- activeUntil: now + 2 * HOUR,
302
- });
303
-
304
- const ctx = buildCtx({
305
- messages: [makeUserMessage("What should I be working on?")],
306
- });
307
- const msgId = uuid();
308
-
309
- const result = await prepareMemoryContext(
310
- ctx,
311
- "What should I be working on?",
312
- msgId,
313
- abortController.signal,
314
- onEvent,
315
- );
316
-
317
- // Should have injected memory_brief into the last user message
318
- const lastMsg = result.runMessages[result.runMessages.length - 1];
319
- expect(lastMsg.role).toBe("user");
320
- const textBlocks = lastMsg.content.filter((b) => b.type === "text");
321
- const injectedText = textBlocks
322
- .map((b) => ("text" in b ? b.text : ""))
323
- .join("\n");
324
-
325
- expect(injectedText).toContain("<memory_brief>");
326
- expect(injectedText).toContain("Deploy to staging at 3pm");
327
- expect(injectedText).toContain("</memory_brief>");
328
-
329
- // Should NOT contain <supporting_recall> (no archive data or trigger)
330
- expect(injectedText).not.toContain("<supporting_recall>");
331
-
332
- // Should have emitted memory_status
333
- expect(events.some((e) => e.type === "memory_status")).toBe(true);
334
- });
335
-
336
- test("injects <memory_brief> with open loops", async () => {
337
- const now = Date.now();
338
- insertOpenLoop({
339
- id: "ol-1",
340
- summary: "Review the PR for the auth refactor",
341
- dueAt: now + 6 * HOUR,
342
- updatedAt: now - DAY * 10,
343
- });
344
-
345
- const ctx = buildCtx({
346
- messages: [makeUserMessage("What are my pending tasks?")],
347
- });
348
- const msgId = uuid();
349
-
350
- const result = await prepareMemoryContext(
351
- ctx,
352
- "What are my pending tasks?",
353
- msgId,
354
- abortController.signal,
355
- onEvent,
356
- );
357
-
358
- const lastMsg = result.runMessages[result.runMessages.length - 1];
359
- const textBlocks = lastMsg.content.filter((b) => b.type === "text");
360
- const injectedText = textBlocks
361
- .map((b) => ("text" in b ? b.text : ""))
362
- .join("\n");
363
-
364
- expect(injectedText).toContain("<memory_brief>");
365
- expect(injectedText).toContain("Review the PR for the auth refactor");
366
- expect(injectedText).toContain("</memory_brief>");
367
- });
368
-
369
- test("returns unmodified messages when brief is empty and no recall", async () => {
370
- // No time contexts or open loops — brief will be empty
371
- const ctx = buildCtx({
372
- messages: [makeUserMessage("Write a function to sort an array")],
373
- });
374
- const msgId = uuid();
375
-
376
- const result = await prepareMemoryContext(
377
- ctx,
378
- "Write a function to sort an array",
379
- msgId,
380
- abortController.signal,
381
- onEvent,
382
- );
383
-
384
- // Messages should be unmodified
385
- expect(result.runMessages).toEqual(ctx.messages);
386
- expect(result.recall.injectedText).toBe("");
387
- });
388
- });
389
-
390
- // ── Brief-plus-recall turns ─────────────────────────────────────
391
-
392
- describe("brief-plus-recall turns", () => {
393
- test("injects both <memory_brief> and <supporting_recall>", async () => {
394
- const now = Date.now();
395
-
396
- // Seed time context for the brief
397
- insertTimeContext({
398
- id: "tc-1",
399
- summary: "Code review session at 4pm",
400
- activeFrom: now - HOUR,
401
- activeUntil: now + 3 * HOUR,
402
- });
403
-
404
- // Seed archive data that will trigger recall
405
- const convId = uuid();
406
- const msgId = uuid();
407
- createConversation(convId, "Authentication Discussion");
408
- createMessage(msgId, convId);
409
-
410
- insertObservation({
411
- conversationId: convId,
412
- messageId: msgId,
413
- role: "user",
414
- content:
415
- "User wants to migrate authentication from JWT to session tokens",
416
- scopeId: "default",
417
- });
418
-
419
- const ctx = buildCtx({
420
- messages: [
421
- makeUserMessage(
422
- "Do you remember what we discussed about authentication?",
423
- ),
424
- ],
425
- });
426
- const userMsgId = uuid();
427
-
428
- const result = await prepareMemoryContext(
429
- ctx,
430
- "Do you remember what we discussed about authentication?",
431
- userMsgId,
432
- abortController.signal,
433
- onEvent,
434
- );
435
-
436
- const lastMsg = result.runMessages[result.runMessages.length - 1];
437
- const textBlocks = lastMsg.content.filter((b) => b.type === "text");
438
- const injectedText = textBlocks
439
- .map((b) => ("text" in b ? b.text : ""))
440
- .join("\n");
441
-
442
- // Both blocks should be present
443
- expect(injectedText).toContain("<memory_brief>");
444
- expect(injectedText).toContain("Code review session at 4pm");
445
- expect(injectedText).toContain("</memory_brief>");
446
- expect(injectedText).toContain("<supporting_recall>");
447
- expect(injectedText).toContain("authentication");
448
- expect(injectedText).toContain("</supporting_recall>");
449
- });
450
-
451
- test("injects only <supporting_recall> when brief is empty but recall triggers", async () => {
452
- // No time contexts or open loops, so brief is empty
453
- const convId = uuid();
454
- const msgId = uuid();
455
- createConversation(convId, "Database Planning");
456
- createMessage(msgId, convId);
457
-
458
- insertCompactionEpisode({
459
- scopeId: "default",
460
- conversationId: convId,
461
- title: "PostgreSQL Migration",
462
- summary:
463
- "Discussed migrating from MySQL to PostgreSQL with a phased approach",
464
- tokenEstimate: 25,
465
- startAt: Date.now() - DAY,
466
- endAt: Date.now() - 12 * HOUR,
467
- });
468
-
469
- const ctx = buildCtx({
470
- messages: [
471
- makeUserMessage("Do you remember the PostgreSQL migration plan?"),
472
- ],
473
- });
474
- const userMsgId = uuid();
475
-
476
- const result = await prepareMemoryContext(
477
- ctx,
478
- "Do you remember the PostgreSQL migration plan?",
479
- userMsgId,
480
- abortController.signal,
481
- onEvent,
482
- );
483
-
484
- const lastMsg = result.runMessages[result.runMessages.length - 1];
485
- const textBlocks = lastMsg.content.filter((b) => b.type === "text");
486
- const injectedText = textBlocks
487
- .map((b) => ("text" in b ? b.text : ""))
488
- .join("\n");
489
-
490
- // Brief should not be present (empty)
491
- expect(injectedText).not.toContain("<memory_brief>");
492
- // Recall should be present
493
- expect(injectedText).toContain("<supporting_recall>");
494
- expect(injectedText).toContain("PostgreSQL");
495
- expect(injectedText).toContain("</supporting_recall>");
496
- });
497
- });
498
-
499
- // ── Disabled-flag fallback ──────────────────────────────────────
500
-
501
- describe("disabled-flag fallback", () => {
502
- test("falls back to legacy path when memory.simplified.enabled is false", async () => {
503
- // Disable the simplified flag
504
- testConfig = {
505
- ...DEFAULT_CONFIG,
506
- memory: {
507
- ...DEFAULT_CONFIG.memory,
508
- enabled: true,
509
- simplified: {
510
- ...DEFAULT_CONFIG.memory.simplified,
511
- enabled: false,
512
- },
513
- },
514
- };
515
-
516
- // The legacy retriever mock throws, so we need to unmock it for
517
- // this test. Instead, we verify the flag gating by checking the
518
- // code path via the mock that throws — if the simplified path is
519
- // correctly bypassed, the legacy path will be invoked.
520
- const ctx = buildCtx({
521
- messages: [makeUserMessage("Hello there")],
522
- });
523
- const msgId = uuid();
524
-
525
- // The legacy path calls buildMemoryRecall which we mocked to throw.
526
- // This confirms the code took the legacy path, not the simplified one.
527
- let hitLegacyPath = false;
528
- try {
529
- await prepareMemoryContext(
530
- ctx,
531
- "Hello there",
532
- msgId,
533
- abortController.signal,
534
- onEvent,
535
- );
536
- } catch (err) {
537
- if (
538
- err instanceof Error &&
539
- err.message.includes("should not be called in simplified mode")
540
- ) {
541
- hitLegacyPath = true;
542
- } else {
543
- throw err;
544
- }
545
- }
546
-
547
- expect(hitLegacyPath).toBe(true);
548
- });
549
- });
550
-
551
- // ── Gate checks ─────────────────────────────────────────────────
552
-
553
- describe("gate checks", () => {
554
- test("skips memory for untrusted actors in simplified mode", async () => {
555
- const now = Date.now();
556
- insertTimeContext({
557
- id: "tc-1",
558
- summary: "Meeting in 1 hour",
559
- activeFrom: now - HOUR,
560
- activeUntil: now + HOUR,
561
- });
562
-
563
- const ctx = buildCtx({
564
- trustClass: "unknown",
565
- messages: [makeUserMessage("What are my meetings?")],
566
- });
567
- const msgId = uuid();
568
-
569
- const result = await prepareMemoryContext(
570
- ctx,
571
- "What are my meetings?",
572
- msgId,
573
- abortController.signal,
574
- onEvent,
575
- );
576
-
577
- // Should return unmodified messages (no memory injection)
578
- expect(result.runMessages).toEqual(ctx.messages);
579
- expect(result.recall.enabled).toBe(false);
580
- });
581
-
582
- test("skips memory for tool-result-only turns in simplified mode", async () => {
583
- const now = Date.now();
584
- insertTimeContext({
585
- id: "tc-1",
586
- summary: "Important deadline",
587
- activeFrom: now - HOUR,
588
- activeUntil: now + HOUR,
589
- });
590
-
591
- const toolResultMsg: Message = {
592
- role: "user",
593
- content: [
594
- {
595
- type: "tool_result",
596
- tool_use_id: "tool-1",
597
- content: "tool output",
598
- },
599
- ],
600
- };
601
- const ctx = buildCtx({ messages: [toolResultMsg] });
602
- const msgId = uuid();
603
-
604
- const result = await prepareMemoryContext(
605
- ctx,
606
- "",
607
- msgId,
608
- abortController.signal,
609
- onEvent,
610
- );
611
-
612
- // Should return unmodified messages
613
- expect(result.runMessages).toEqual(ctx.messages);
614
- });
615
- });
616
- });
@@ -1,101 +0,0 @@
1
- import { z } from "zod";
2
-
3
- export const MemorySimplifiedBriefConfigSchema = z
4
- .object({
5
- maxTokens: z
6
- .number({
7
- error: "memory.simplified.brief.maxTokens must be a number",
8
- })
9
- .int("memory.simplified.brief.maxTokens must be an integer")
10
- .positive("memory.simplified.brief.maxTokens must be a positive integer")
11
- .default(4000)
12
- .describe(
13
- "Maximum token budget for the memory brief injected into conversation context",
14
- ),
15
- })
16
- .describe("Controls the memory brief that is injected into conversations");
17
-
18
- export const MemorySimplifiedReducerConfigSchema = z
19
- .object({
20
- idleDelayMs: z
21
- .number({
22
- error: "memory.simplified.reducer.idleDelayMs must be a number",
23
- })
24
- .int("memory.simplified.reducer.idleDelayMs must be an integer")
25
- .positive(
26
- "memory.simplified.reducer.idleDelayMs must be a positive integer",
27
- )
28
- .default(30_000)
29
- .describe(
30
- "Milliseconds of idle time before the reducer processes new conversation turns into memory",
31
- ),
32
- switchWaitMs: z
33
- .number({
34
- error: "memory.simplified.reducer.switchWaitMs must be a number",
35
- })
36
- .int("memory.simplified.reducer.switchWaitMs must be an integer")
37
- .positive(
38
- "memory.simplified.reducer.switchWaitMs must be a positive integer",
39
- )
40
- .default(5_000)
41
- .describe(
42
- "Milliseconds to wait after a conversation switch before running the reducer",
43
- ),
44
- })
45
- .describe(
46
- "Controls when the memory reducer runs to process conversation turns into persistent memory",
47
- );
48
-
49
- export const MemorySimplifiedArchiveRecallConfigSchema = z
50
- .object({
51
- maxSnippets: z
52
- .number({
53
- error: "memory.simplified.archiveRecall.maxSnippets must be a number",
54
- })
55
- .int("memory.simplified.archiveRecall.maxSnippets must be an integer")
56
- .positive(
57
- "memory.simplified.archiveRecall.maxSnippets must be a positive integer",
58
- )
59
- .default(10)
60
- .describe(
61
- "Maximum number of archive snippets to recall when supplementing the brief with semantic search",
62
- ),
63
- })
64
- .describe(
65
- "Controls how archived memory snippets are recalled via semantic search",
66
- );
67
-
68
- export const MemorySimplifiedConfigSchema = z
69
- .object({
70
- enabled: z
71
- .boolean({
72
- error: "memory.simplified.enabled must be a boolean",
73
- })
74
- .default(true)
75
- .describe("Whether the simplified memory system is enabled"),
76
- brief: MemorySimplifiedBriefConfigSchema.default(
77
- MemorySimplifiedBriefConfigSchema.parse({}),
78
- ),
79
- reducer: MemorySimplifiedReducerConfigSchema.default(
80
- MemorySimplifiedReducerConfigSchema.parse({}),
81
- ),
82
- archiveRecall: MemorySimplifiedArchiveRecallConfigSchema.default(
83
- MemorySimplifiedArchiveRecallConfigSchema.parse({}),
84
- ),
85
- })
86
- .describe(
87
- "Simplified two-layer memory system — a brief plus archive recall, replacing the legacy item/tier/staleness model",
88
- );
89
-
90
- export type MemorySimplifiedConfig = z.infer<
91
- typeof MemorySimplifiedConfigSchema
92
- >;
93
- export type MemorySimplifiedBriefConfig = z.infer<
94
- typeof MemorySimplifiedBriefConfigSchema
95
- >;
96
- export type MemorySimplifiedReducerConfig = z.infer<
97
- typeof MemorySimplifiedReducerConfigSchema
98
- >;
99
- export type MemorySimplifiedArchiveRecallConfig = z.infer<
100
- typeof MemorySimplifiedArchiveRecallConfigSchema
101
- >;