@vellumai/assistant 0.5.5 → 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 (102) hide show
  1. package/Dockerfile +3 -4
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-token-service.test.ts +113 -0
  4. package/src/__tests__/config-schema.test.ts +2 -2
  5. package/src/__tests__/context-window-manager.test.ts +78 -0
  6. package/src/__tests__/conversation-title-service.test.ts +30 -1
  7. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  8. package/src/__tests__/memory-regressions.test.ts +8 -30
  9. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  10. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  11. package/src/__tests__/tool-executor.test.ts +4 -0
  12. package/src/cli/commands/conversations.ts +0 -18
  13. package/src/config/env.ts +8 -2
  14. package/src/config/feature-flag-registry.json +0 -8
  15. package/src/config/schema.ts +0 -12
  16. package/src/config/schemas/memory.ts +0 -4
  17. package/src/config/schemas/platform.ts +1 -1
  18. package/src/config/schemas/security.ts +4 -0
  19. package/src/context/window-manager.ts +53 -2
  20. package/src/daemon/config-watcher.ts +1 -4
  21. package/src/daemon/conversation-agent-loop.ts +0 -60
  22. package/src/daemon/conversation-memory.ts +0 -117
  23. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  24. package/src/daemon/handlers/conversations.ts +0 -11
  25. package/src/daemon/lifecycle.ts +3 -46
  26. package/src/followups/followup-store.ts +5 -2
  27. package/src/memory/conversation-crud.ts +0 -236
  28. package/src/memory/conversation-title-service.ts +26 -10
  29. package/src/memory/db-init.ts +5 -13
  30. package/src/memory/indexer.ts +15 -106
  31. package/src/memory/job-handlers/embedding.ts +0 -79
  32. package/src/memory/job-utils.ts +1 -1
  33. package/src/memory/jobs-store.ts +0 -8
  34. package/src/memory/jobs-worker.ts +0 -20
  35. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  36. package/src/memory/migrations/index.ts +1 -3
  37. package/src/memory/qdrant-client.ts +4 -6
  38. package/src/memory/schema/conversations.ts +0 -3
  39. package/src/memory/schema/index.ts +0 -2
  40. package/src/messaging/draft-store.ts +2 -2
  41. package/src/permissions/defaults.ts +3 -3
  42. package/src/permissions/trust-client.ts +2 -13
  43. package/src/permissions/trust-store.ts +8 -3
  44. package/src/runtime/auth/route-policy.ts +14 -0
  45. package/src/runtime/auth/token-service.ts +133 -0
  46. package/src/runtime/http-server.ts +2 -0
  47. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  48. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  49. package/src/runtime/routes/conversation-routes.ts +2 -1
  50. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  51. package/src/runtime/routes/memory-item-routes.ts +124 -2
  52. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  53. package/src/schedule/schedule-store.ts +0 -21
  54. package/src/skills/inline-command-render.ts +5 -1
  55. package/src/skills/inline-command-runner.ts +30 -2
  56. package/src/tools/memory/handlers.ts +1 -129
  57. package/src/tools/permission-checker.ts +18 -0
  58. package/src/tools/skills/load.ts +9 -2
  59. package/src/util/platform.ts +5 -5
  60. package/src/util/xml.ts +8 -0
  61. package/src/workspace/heartbeat-service.ts +5 -24
  62. package/src/__tests__/archive-recall.test.ts +0 -560
  63. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  64. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  65. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  66. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  67. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  68. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  69. package/src/__tests__/memory-brief-time.test.ts +0 -285
  70. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  71. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  72. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  73. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  74. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  75. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  76. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  77. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  78. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  79. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  80. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  81. package/src/__tests__/memory-reducer.test.ts +0 -704
  82. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  83. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  84. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  85. package/src/config/schemas/memory-simplified.ts +0 -101
  86. package/src/memory/archive-recall.ts +0 -516
  87. package/src/memory/archive-store.ts +0 -400
  88. package/src/memory/brief-formatting.ts +0 -33
  89. package/src/memory/brief-open-loops.ts +0 -266
  90. package/src/memory/brief-time.ts +0 -162
  91. package/src/memory/brief.ts +0 -75
  92. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  93. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  94. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  95. package/src/memory/migrations/186-memory-archive.ts +0 -109
  96. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  97. package/src/memory/reducer-scheduler.ts +0 -242
  98. package/src/memory/reducer-store.ts +0 -271
  99. package/src/memory/reducer-types.ts +0 -106
  100. package/src/memory/reducer.ts +0 -467
  101. package/src/memory/schema/memory-archive.ts +0 -121
  102. package/src/memory/schema/memory-brief.ts +0 -55
@@ -1,473 +0,0 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
-
6
- // ── Test directory & platform mocks ───────────────────────────────
7
-
8
- const testDir = mkdtempSync(join(tmpdir(), "reducer-scheduling-test-"));
9
-
10
- mock.module("../util/platform.js", () => ({
11
- getDataDir: () => testDir,
12
- getRootDir: () => testDir,
13
- getWorkspaceDir: () => join(testDir, "workspace"),
14
- getConversationsDir: () => join(testDir, "workspace", "conversations"),
15
- isMacOS: () => process.platform === "darwin",
16
- isLinux: () => process.platform === "linux",
17
- isWindows: () => process.platform === "win32",
18
- getPidPath: () => join(testDir, "test.pid"),
19
- getDbPath: () => join(testDir, "test.db"),
20
- getLogPath: () => join(testDir, "test.log"),
21
- ensureDataDir: () => {},
22
- }));
23
-
24
- mock.module("../util/logger.js", () => ({
25
- getLogger: () =>
26
- new Proxy({} as Record<string, unknown>, {
27
- get: () => () => {},
28
- }),
29
- }));
30
-
31
- // ── Config mock — controllable idleDelayMs ───────────────────────
32
-
33
- let mockIdleDelayMs = 30_000;
34
-
35
- mock.module("../config/loader.js", () => ({
36
- getConfig: () => ({
37
- memory: {
38
- simplified: {
39
- reducer: {
40
- idleDelayMs: mockIdleDelayMs,
41
- switchWaitMs: 5_000,
42
- },
43
- },
44
- },
45
- }),
46
- loadConfig: () => ({
47
- memory: {
48
- simplified: {
49
- reducer: {
50
- idleDelayMs: mockIdleDelayMs,
51
- switchWaitMs: 5_000,
52
- },
53
- },
54
- },
55
- }),
56
- }));
57
-
58
- // ── Suppress disk-view side effects ──────────────────────────────
59
-
60
- mock.module("../memory/conversation-disk-view.js", () => ({
61
- initConversationDir: () => {},
62
- removeConversationDir: () => {},
63
- syncMessageToDisk: () => {},
64
- updateMetaFile: () => {},
65
- }));
66
-
67
- // ── Suppress indexer side effects ────────────────────────────────
68
-
69
- mock.module("../memory/indexer.js", () => ({
70
- indexMessageNow: async () => {},
71
- }));
72
-
73
- // ── Suppress attention side effects ──────────────────────────────
74
-
75
- mock.module("../memory/conversation-attention-store.js", () => ({
76
- projectAssistantMessage: () => {},
77
- seedForkedConversationAttention: () => {},
78
- }));
79
-
80
- // ── Imports (after mocks) ─────────────────────────────────────────
81
-
82
- import {
83
- markConversationMemoryDirty,
84
- scheduleReducerJob,
85
- sweepStaleReducerJobs,
86
- } from "../memory/conversation-crud.js";
87
- import { initializeDb, resetDb } from "../memory/db.js";
88
- import { getSqlite } from "../memory/db-connection.js";
89
- import { resetTestTables } from "../memory/raw-query.js";
90
-
91
- initializeDb();
92
-
93
- // ── Helpers ───────────────────────────────────────────────────────
94
-
95
- const NOW = 1_700_000_000_000;
96
-
97
- function insertConversation(
98
- id: string,
99
- opts?: {
100
- dirtyTailMessageId?: string | null;
101
- createdAt?: number;
102
- },
103
- ): void {
104
- const raw = getSqlite();
105
- raw.run(
106
- `INSERT INTO conversations (id, title, created_at, updated_at, conversation_type, source, memory_scope_id, is_auto_title,
107
- memory_dirty_tail_since_message_id)
108
- VALUES (?, 'Test', ?, ?, 'standard', 'user', 'default', 1, ?)`,
109
- [
110
- id,
111
- opts?.createdAt ?? NOW,
112
- opts?.createdAt ?? NOW,
113
- opts?.dirtyTailMessageId ?? null,
114
- ],
115
- );
116
- }
117
-
118
- function insertMessage(opts: {
119
- id: string;
120
- conversationId: string;
121
- role?: string;
122
- content?: string;
123
- createdAt?: number;
124
- }): void {
125
- const raw = getSqlite();
126
- raw.run(
127
- `INSERT INTO messages (id, conversation_id, role, content, created_at)
128
- VALUES (?, ?, ?, ?, ?)`,
129
- [
130
- opts.id,
131
- opts.conversationId,
132
- opts.role ?? "user",
133
- opts.content ?? "test message",
134
- opts.createdAt ?? NOW,
135
- ],
136
- );
137
- }
138
-
139
- function getReducerJobs(
140
- conversationId: string,
141
- ): Array<Record<string, unknown>> {
142
- const raw = getSqlite();
143
- return raw
144
- .query(
145
- `SELECT * FROM memory_jobs
146
- WHERE type = 'reduce_conversation_memory'
147
- AND json_extract(payload, '$.conversationId') = ?
148
- ORDER BY created_at ASC`,
149
- )
150
- .all(conversationId) as Array<Record<string, unknown>>;
151
- }
152
-
153
- function insertReducerJob(
154
- conversationId: string,
155
- opts?: { status?: string; runAfter?: number },
156
- ): void {
157
- const raw = getSqlite();
158
- const now = Date.now();
159
- raw.run(
160
- `INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, created_at, updated_at)
161
- VALUES (?, 'reduce_conversation_memory', ?, ?, 0, 0, ?, ?, ?)`,
162
- [
163
- `job-${conversationId}-${now}`,
164
- JSON.stringify({ conversationId }),
165
- opts?.status ?? "pending",
166
- opts?.runAfter ?? now + 30_000,
167
- now,
168
- now,
169
- ],
170
- );
171
- }
172
-
173
- // ── Teardown ──────────────────────────────────────────────────────
174
-
175
- afterAll(() => {
176
- resetDb();
177
- try {
178
- rmSync(testDir, { recursive: true });
179
- } catch {
180
- /* best effort */
181
- }
182
- });
183
-
184
- beforeEach(() => {
185
- resetTestTables("messages", "conversations", "memory_jobs");
186
- mockIdleDelayMs = 30_000;
187
- });
188
-
189
- // ── Tests ─────────────────────────────────────────────────────────
190
-
191
- describe("markConversationMemoryDirty — reducer job scheduling", () => {
192
- test("creates a pending reducer job on first dirty mark", () => {
193
- insertConversation("conv-1");
194
- insertMessage({ id: "msg-1", conversationId: "conv-1" });
195
-
196
- markConversationMemoryDirty("conv-1", "msg-1");
197
-
198
- const jobs = getReducerJobs("conv-1");
199
- expect(jobs).toHaveLength(1);
200
- expect(jobs[0].status).toBe("pending");
201
- expect(jobs[0].type).toBe("reduce_conversation_memory");
202
- });
203
-
204
- test("schedules reducer job with idleDelayMs offset from now", () => {
205
- mockIdleDelayMs = 60_000;
206
- insertConversation("conv-1");
207
- insertMessage({ id: "msg-1", conversationId: "conv-1" });
208
-
209
- const before = Date.now();
210
- markConversationMemoryDirty("conv-1", "msg-1");
211
- const after = Date.now();
212
-
213
- const jobs = getReducerJobs("conv-1");
214
- expect(jobs).toHaveLength(1);
215
- const runAfter = jobs[0].run_after as number;
216
- // runAfter should be approximately now + 60_000
217
- expect(runAfter).toBeGreaterThanOrEqual(before + 60_000);
218
- expect(runAfter).toBeLessThanOrEqual(after + 60_000);
219
- });
220
-
221
- test("deduplicates: second mark does not create a second job", () => {
222
- insertConversation("conv-1");
223
- insertMessage({ id: "msg-1", conversationId: "conv-1" });
224
- insertMessage({
225
- id: "msg-2",
226
- conversationId: "conv-1",
227
- createdAt: NOW + 1000,
228
- });
229
-
230
- markConversationMemoryDirty("conv-1", "msg-1");
231
- markConversationMemoryDirty("conv-1", "msg-2");
232
-
233
- const jobs = getReducerJobs("conv-1");
234
- expect(jobs).toHaveLength(1);
235
- });
236
-
237
- test("reschedules: second mark pushes runAfter forward", () => {
238
- mockIdleDelayMs = 10_000;
239
- insertConversation("conv-1");
240
- insertMessage({ id: "msg-1", conversationId: "conv-1" });
241
-
242
- markConversationMemoryDirty("conv-1", "msg-1");
243
- const jobs1 = getReducerJobs("conv-1");
244
- const firstRunAfter = jobs1[0].run_after as number;
245
-
246
- // Simulate a short delay before the next message
247
- const pauseMs = 50;
248
- Bun.sleepSync(pauseMs);
249
-
250
- insertMessage({
251
- id: "msg-2",
252
- conversationId: "conv-1",
253
- createdAt: NOW + 5000,
254
- });
255
- markConversationMemoryDirty("conv-1", "msg-2");
256
-
257
- const jobs2 = getReducerJobs("conv-1");
258
- expect(jobs2).toHaveLength(1);
259
- const secondRunAfter = jobs2[0].run_after as number;
260
- // The second runAfter should be later than the first
261
- expect(secondRunAfter).toBeGreaterThan(firstRunAfter);
262
- });
263
-
264
- test("creates separate jobs for different conversations", () => {
265
- insertConversation("conv-1");
266
- insertConversation("conv-2");
267
- insertMessage({ id: "msg-1", conversationId: "conv-1" });
268
- insertMessage({ id: "msg-2", conversationId: "conv-2" });
269
-
270
- markConversationMemoryDirty("conv-1", "msg-1");
271
- markConversationMemoryDirty("conv-2", "msg-2");
272
-
273
- const jobs1 = getReducerJobs("conv-1");
274
- const jobs2 = getReducerJobs("conv-2");
275
- expect(jobs1).toHaveLength(1);
276
- expect(jobs2).toHaveLength(1);
277
- // They should be different job rows
278
- expect(jobs1[0].id).not.toBe(jobs2[0].id);
279
- });
280
-
281
- test("does not reschedule completed or failed jobs", () => {
282
- insertConversation("conv-1");
283
- insertMessage({ id: "msg-1", conversationId: "conv-1" });
284
-
285
- // Insert a completed job for this conversation
286
- insertReducerJob("conv-1", { status: "completed" });
287
-
288
- markConversationMemoryDirty("conv-1", "msg-1");
289
-
290
- // Should create a new pending job (not reuse the completed one)
291
- const jobs = getReducerJobs("conv-1");
292
- const pendingJobs = jobs.filter((j) => j.status === "pending");
293
- expect(pendingJobs).toHaveLength(1);
294
- });
295
-
296
- test("does not reschedule running jobs", () => {
297
- insertConversation("conv-1");
298
- insertMessage({ id: "msg-1", conversationId: "conv-1" });
299
-
300
- // Insert a running job for this conversation
301
- insertReducerJob("conv-1", { status: "running" });
302
-
303
- markConversationMemoryDirty("conv-1", "msg-1");
304
-
305
- // Should create a new pending job since we only look at pending for rescheduling
306
- const jobs = getReducerJobs("conv-1");
307
- const pendingJobs = jobs.filter((j) => j.status === "pending");
308
- expect(pendingJobs).toHaveLength(1);
309
- });
310
- });
311
-
312
- describe("scheduleReducerJob — explicit runAfter override", () => {
313
- test("accepts a custom runAfter timestamp", () => {
314
- insertConversation("conv-1");
315
-
316
- const customRunAfter = NOW + 999_999;
317
- scheduleReducerJob("conv-1", customRunAfter);
318
-
319
- const jobs = getReducerJobs("conv-1");
320
- expect(jobs).toHaveLength(1);
321
- expect(jobs[0].run_after).toBe(customRunAfter);
322
- });
323
- });
324
-
325
- describe("sweepStaleReducerJobs — startup sweep", () => {
326
- test("enqueues immediate jobs for stale dirty conversations", () => {
327
- mockIdleDelayMs = 30_000;
328
- const oldTime = Date.now() - 60_000; // Well past idle delay
329
-
330
- insertConversation("conv-1", { dirtyTailMessageId: "msg-1" });
331
- insertMessage({
332
- id: "msg-1",
333
- conversationId: "conv-1",
334
- createdAt: oldTime,
335
- });
336
-
337
- const count = sweepStaleReducerJobs();
338
-
339
- expect(count).toBe(1);
340
- const jobs = getReducerJobs("conv-1");
341
- expect(jobs).toHaveLength(1);
342
- expect(jobs[0].status).toBe("pending");
343
- // Should be scheduled for immediate execution (runAfter <= now)
344
- expect(jobs[0].run_after as number).toBeLessThanOrEqual(Date.now());
345
- });
346
-
347
- test("skips conversations that are not dirty", () => {
348
- const oldTime = Date.now() - 60_000;
349
-
350
- // Not dirty — no dirtyTailMessageId
351
- insertConversation("conv-1");
352
- insertMessage({
353
- id: "msg-1",
354
- conversationId: "conv-1",
355
- createdAt: oldTime,
356
- });
357
-
358
- const count = sweepStaleReducerJobs();
359
- expect(count).toBe(0);
360
- expect(getReducerJobs("conv-1")).toHaveLength(0);
361
- });
362
-
363
- test("skips dirty conversations whose tail is within the idle window", () => {
364
- mockIdleDelayMs = 30_000;
365
- const recentTime = Date.now() - 5_000; // Only 5s ago, within idle delay
366
-
367
- insertConversation("conv-1", { dirtyTailMessageId: "msg-1" });
368
- insertMessage({
369
- id: "msg-1",
370
- conversationId: "conv-1",
371
- createdAt: recentTime,
372
- });
373
-
374
- const count = sweepStaleReducerJobs();
375
- expect(count).toBe(0);
376
- expect(getReducerJobs("conv-1")).toHaveLength(0);
377
- });
378
-
379
- test("skips conversations that already have a pending reducer job", () => {
380
- mockIdleDelayMs = 30_000;
381
- const oldTime = Date.now() - 60_000;
382
-
383
- insertConversation("conv-1", { dirtyTailMessageId: "msg-1" });
384
- insertMessage({
385
- id: "msg-1",
386
- conversationId: "conv-1",
387
- createdAt: oldTime,
388
- });
389
- insertReducerJob("conv-1", { status: "pending" });
390
-
391
- const count = sweepStaleReducerJobs();
392
- expect(count).toBe(0);
393
- // Only the pre-existing job should be there
394
- const jobs = getReducerJobs("conv-1");
395
- expect(jobs).toHaveLength(1);
396
- });
397
-
398
- test("skips conversations that have a running reducer job", () => {
399
- mockIdleDelayMs = 30_000;
400
- const oldTime = Date.now() - 60_000;
401
-
402
- insertConversation("conv-1", { dirtyTailMessageId: "msg-1" });
403
- insertMessage({
404
- id: "msg-1",
405
- conversationId: "conv-1",
406
- createdAt: oldTime,
407
- });
408
- insertReducerJob("conv-1", { status: "running" });
409
-
410
- const count = sweepStaleReducerJobs();
411
- expect(count).toBe(0);
412
- });
413
-
414
- test("sweeps multiple stale conversations", () => {
415
- mockIdleDelayMs = 30_000;
416
- const oldTime = Date.now() - 60_000;
417
-
418
- insertConversation("conv-1", { dirtyTailMessageId: "msg-1" });
419
- insertMessage({
420
- id: "msg-1",
421
- conversationId: "conv-1",
422
- createdAt: oldTime,
423
- });
424
-
425
- insertConversation("conv-2", { dirtyTailMessageId: "msg-2" });
426
- insertMessage({
427
- id: "msg-2",
428
- conversationId: "conv-2",
429
- createdAt: oldTime - 1000,
430
- });
431
-
432
- const count = sweepStaleReducerJobs();
433
- expect(count).toBe(2);
434
- expect(getReducerJobs("conv-1")).toHaveLength(1);
435
- expect(getReducerJobs("conv-2")).toHaveLength(1);
436
- });
437
-
438
- test("only enqueues for stale conversations in a mixed set", () => {
439
- mockIdleDelayMs = 30_000;
440
- const oldTime = Date.now() - 60_000;
441
- const recentTime = Date.now() - 5_000;
442
-
443
- // Stale dirty conversation
444
- insertConversation("conv-stale", { dirtyTailMessageId: "msg-1" });
445
- insertMessage({
446
- id: "msg-1",
447
- conversationId: "conv-stale",
448
- createdAt: oldTime,
449
- });
450
-
451
- // Recent dirty conversation (within idle window)
452
- insertConversation("conv-recent", { dirtyTailMessageId: "msg-2" });
453
- insertMessage({
454
- id: "msg-2",
455
- conversationId: "conv-recent",
456
- createdAt: recentTime,
457
- });
458
-
459
- // Clean conversation
460
- insertConversation("conv-clean");
461
- insertMessage({
462
- id: "msg-3",
463
- conversationId: "conv-clean",
464
- createdAt: oldTime,
465
- });
466
-
467
- const count = sweepStaleReducerJobs();
468
- expect(count).toBe(1);
469
- expect(getReducerJobs("conv-stale")).toHaveLength(1);
470
- expect(getReducerJobs("conv-recent")).toHaveLength(0);
471
- expect(getReducerJobs("conv-clean")).toHaveLength(0);
472
- });
473
- });