@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,150 +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
|
-
const testDir = mkdtempSync(join(tmpdir(), "conv-dirty-tail-test-"));
|
|
7
|
-
|
|
8
|
-
mock.module("../util/platform.js", () => ({
|
|
9
|
-
getDataDir: () => testDir,
|
|
10
|
-
isMacOS: () => process.platform === "darwin",
|
|
11
|
-
isLinux: () => process.platform === "linux",
|
|
12
|
-
isWindows: () => process.platform === "win32",
|
|
13
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
14
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
15
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
16
|
-
ensureDataDir: () => {},
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
mock.module("../util/logger.js", () => ({
|
|
20
|
-
getLogger: () =>
|
|
21
|
-
new Proxy({} as Record<string, unknown>, {
|
|
22
|
-
get: () => () => {},
|
|
23
|
-
}),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
addMessage,
|
|
28
|
-
createConversation,
|
|
29
|
-
getConversation,
|
|
30
|
-
getMessages,
|
|
31
|
-
markConversationMemoryDirty,
|
|
32
|
-
} from "../memory/conversation-crud.js";
|
|
33
|
-
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
34
|
-
|
|
35
|
-
initializeDb();
|
|
36
|
-
|
|
37
|
-
afterAll(() => {
|
|
38
|
-
resetDb();
|
|
39
|
-
try {
|
|
40
|
-
rmSync(testDir, { recursive: true });
|
|
41
|
-
} catch {
|
|
42
|
-
/* best effort */
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("markConversationMemoryDirty", () => {
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
const db = getDb();
|
|
49
|
-
db.run(`DELETE FROM messages`);
|
|
50
|
-
db.run(`DELETE FROM conversations`);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("first message marks the conversation dirty with its message ID", async () => {
|
|
54
|
-
const conv = createConversation("test");
|
|
55
|
-
const msg = await addMessage(conv.id, "user", "hello world", undefined, {
|
|
56
|
-
skipIndexing: true,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const updated = getConversation(conv.id);
|
|
60
|
-
expect(updated).not.toBeNull();
|
|
61
|
-
expect(updated!.memoryDirtyTailSinceMessageId).toBe(msg.id);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("repeated messages preserve the original dirty boundary", async () => {
|
|
65
|
-
const conv = createConversation("test");
|
|
66
|
-
const msg1 = await addMessage(conv.id, "user", "first message", undefined, {
|
|
67
|
-
skipIndexing: true,
|
|
68
|
-
});
|
|
69
|
-
const msg2 = await addMessage(
|
|
70
|
-
conv.id,
|
|
71
|
-
"assistant",
|
|
72
|
-
"second message",
|
|
73
|
-
undefined,
|
|
74
|
-
{ skipIndexing: true },
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const updated = getConversation(conv.id);
|
|
78
|
-
expect(updated).not.toBeNull();
|
|
79
|
-
// The dirty tail should still point to msg1, not msg2.
|
|
80
|
-
expect(updated!.memoryDirtyTailSinceMessageId).toBe(msg1.id);
|
|
81
|
-
// msg2 should still be persisted normally.
|
|
82
|
-
expect(msg2.id).not.toBe(msg1.id);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("markConversationMemoryDirty is a no-op when already dirty", () => {
|
|
86
|
-
const conv = createConversation("test");
|
|
87
|
-
const firstMessageId = "first-msg-id";
|
|
88
|
-
const secondMessageId = "second-msg-id";
|
|
89
|
-
|
|
90
|
-
markConversationMemoryDirty(conv.id, firstMessageId);
|
|
91
|
-
const after1 = getConversation(conv.id);
|
|
92
|
-
expect(after1!.memoryDirtyTailSinceMessageId).toBe(firstMessageId);
|
|
93
|
-
|
|
94
|
-
markConversationMemoryDirty(conv.id, secondMessageId);
|
|
95
|
-
const after2 = getConversation(conv.id);
|
|
96
|
-
// Still points to the first message — boundary preserved.
|
|
97
|
-
expect(after2!.memoryDirtyTailSinceMessageId).toBe(firstMessageId);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("message ordering and persistence semantics are unchanged", async () => {
|
|
101
|
-
const conv = createConversation("test");
|
|
102
|
-
const msg1 = await addMessage(conv.id, "user", "question", undefined, {
|
|
103
|
-
skipIndexing: true,
|
|
104
|
-
});
|
|
105
|
-
const msg2 = await addMessage(conv.id, "assistant", "answer", undefined, {
|
|
106
|
-
skipIndexing: true,
|
|
107
|
-
});
|
|
108
|
-
const msg3 = await addMessage(conv.id, "user", "follow-up", undefined, {
|
|
109
|
-
skipIndexing: true,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const allMessages = getMessages(conv.id);
|
|
113
|
-
expect(allMessages).toHaveLength(3);
|
|
114
|
-
// Messages are ordered by createdAt ascending.
|
|
115
|
-
expect(allMessages[0].id).toBe(msg1.id);
|
|
116
|
-
expect(allMessages[1].id).toBe(msg2.id);
|
|
117
|
-
expect(allMessages[2].id).toBe(msg3.id);
|
|
118
|
-
expect(allMessages[0].content).toBe("question");
|
|
119
|
-
expect(allMessages[1].content).toBe("answer");
|
|
120
|
-
expect(allMessages[2].content).toBe("follow-up");
|
|
121
|
-
// createdAt is monotonically increasing.
|
|
122
|
-
expect(allMessages[1].createdAt).toBeGreaterThan(allMessages[0].createdAt);
|
|
123
|
-
expect(allMessages[2].createdAt).toBeGreaterThan(allMessages[1].createdAt);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("every persisted message marks the conversation dirty", async () => {
|
|
127
|
-
const conv = createConversation("test");
|
|
128
|
-
|
|
129
|
-
// Before any messages, the conversation is not dirty.
|
|
130
|
-
const before = getConversation(conv.id);
|
|
131
|
-
expect(before!.memoryDirtyTailSinceMessageId).toBeNull();
|
|
132
|
-
|
|
133
|
-
// After the first message, it becomes dirty.
|
|
134
|
-
const msg1 = await addMessage(conv.id, "user", "msg1", undefined, {
|
|
135
|
-
skipIndexing: true,
|
|
136
|
-
});
|
|
137
|
-
const after1 = getConversation(conv.id);
|
|
138
|
-
expect(after1!.memoryDirtyTailSinceMessageId).toBe(msg1.id);
|
|
139
|
-
|
|
140
|
-
// After subsequent messages, the dirty boundary stays on msg1.
|
|
141
|
-
await addMessage(conv.id, "assistant", "msg2", undefined, {
|
|
142
|
-
skipIndexing: true,
|
|
143
|
-
});
|
|
144
|
-
await addMessage(conv.id, "user", "msg3", undefined, {
|
|
145
|
-
skipIndexing: true,
|
|
146
|
-
});
|
|
147
|
-
const afterAll = getConversation(conv.id);
|
|
148
|
-
expect(afterAll!.memoryDirtyTailSinceMessageId).toBe(msg1.id);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
@@ -1,474 +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(
|
|
9
|
-
join(tmpdir(), "conversation-switch-memory-reduction-test-"),
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
mock.module("../util/platform.js", () => ({
|
|
13
|
-
getDataDir: () => testDir,
|
|
14
|
-
getRootDir: () => testDir,
|
|
15
|
-
getWorkspaceDir: () => join(testDir, "workspace"),
|
|
16
|
-
getConversationsDir: () => join(testDir, "workspace", "conversations"),
|
|
17
|
-
isMacOS: () => process.platform === "darwin",
|
|
18
|
-
isLinux: () => process.platform === "linux",
|
|
19
|
-
isWindows: () => process.platform === "win32",
|
|
20
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
21
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
22
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
23
|
-
ensureDataDir: () => {},
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
mock.module("../util/logger.js", () => ({
|
|
27
|
-
getLogger: () =>
|
|
28
|
-
new Proxy({} as Record<string, unknown>, {
|
|
29
|
-
get: () => () => {},
|
|
30
|
-
}),
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
// ── Config mock ───────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
mock.module("../config/loader.js", () => ({
|
|
36
|
-
getConfig: () => ({
|
|
37
|
-
memory: {
|
|
38
|
-
simplified: {
|
|
39
|
-
reducer: {
|
|
40
|
-
idleDelayMs: 30_000,
|
|
41
|
-
switchWaitMs: 5_000,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
}),
|
|
46
|
-
loadConfig: () => ({
|
|
47
|
-
memory: {
|
|
48
|
-
simplified: {
|
|
49
|
-
reducer: {
|
|
50
|
-
idleDelayMs: 30_000,
|
|
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
|
-
// ── Mock the reducer ──────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
import type { ReducerPromptInput } from "../memory/reducer.js";
|
|
83
|
-
import type { ReducerResult } from "../memory/reducer-types.js";
|
|
84
|
-
import { EMPTY_REDUCER_RESULT } from "../memory/reducer-types.js";
|
|
85
|
-
|
|
86
|
-
let mockReducerResult: ReducerResult = EMPTY_REDUCER_RESULT;
|
|
87
|
-
let lastReducerInput: ReducerPromptInput | null = null;
|
|
88
|
-
let reducerCallCount = 0;
|
|
89
|
-
|
|
90
|
-
mock.module("../memory/reducer.js", () => ({
|
|
91
|
-
runReducer: async (input: ReducerPromptInput) => {
|
|
92
|
-
lastReducerInput = input;
|
|
93
|
-
reducerCallCount++;
|
|
94
|
-
return mockReducerResult;
|
|
95
|
-
},
|
|
96
|
-
}));
|
|
97
|
-
|
|
98
|
-
// ── Imports (after mocks) ─────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
import { initializeDb, resetDb } from "../memory/db.js";
|
|
101
|
-
import { getSqlite } from "../memory/db-connection.js";
|
|
102
|
-
import { resetTestTables } from "../memory/raw-query.js";
|
|
103
|
-
import {
|
|
104
|
-
findMostRecentDirtyConversation,
|
|
105
|
-
reduceBeforeSwitch,
|
|
106
|
-
} from "../memory/reducer-scheduler.js";
|
|
107
|
-
|
|
108
|
-
initializeDb();
|
|
109
|
-
|
|
110
|
-
// ── Helpers ───────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
const NOW = 1_700_000_000_000;
|
|
113
|
-
const SCOPE = "default";
|
|
114
|
-
|
|
115
|
-
function insertConversation(
|
|
116
|
-
id: string,
|
|
117
|
-
opts?: {
|
|
118
|
-
dirtyTailMessageId?: string | null;
|
|
119
|
-
updatedAt?: number;
|
|
120
|
-
memoryScopeId?: string;
|
|
121
|
-
contextSummary?: string;
|
|
122
|
-
},
|
|
123
|
-
): void {
|
|
124
|
-
const raw = getSqlite();
|
|
125
|
-
raw.run(
|
|
126
|
-
`INSERT INTO conversations (id, title, created_at, updated_at, conversation_type, source, memory_scope_id, is_auto_title,
|
|
127
|
-
memory_dirty_tail_since_message_id, context_summary)
|
|
128
|
-
VALUES (?, 'Test', ?, ?, 'standard', 'user', ?, 1, ?, ?)`,
|
|
129
|
-
[
|
|
130
|
-
id,
|
|
131
|
-
NOW,
|
|
132
|
-
opts?.updatedAt ?? NOW,
|
|
133
|
-
opts?.memoryScopeId ?? SCOPE,
|
|
134
|
-
opts?.dirtyTailMessageId ?? null,
|
|
135
|
-
opts?.contextSummary ?? null,
|
|
136
|
-
],
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function insertMessage(opts: {
|
|
141
|
-
id: string;
|
|
142
|
-
conversationId: string;
|
|
143
|
-
role?: string;
|
|
144
|
-
content?: string;
|
|
145
|
-
createdAt?: number;
|
|
146
|
-
}): void {
|
|
147
|
-
const raw = getSqlite();
|
|
148
|
-
raw.run(
|
|
149
|
-
`INSERT INTO messages (id, conversation_id, role, content, created_at)
|
|
150
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
151
|
-
[
|
|
152
|
-
opts.id,
|
|
153
|
-
opts.conversationId,
|
|
154
|
-
opts.role ?? "user",
|
|
155
|
-
opts.content ?? "test message",
|
|
156
|
-
opts.createdAt ?? NOW,
|
|
157
|
-
],
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function getRawConversation(conversationId: string): Record<string, unknown> {
|
|
162
|
-
const raw = getSqlite();
|
|
163
|
-
return raw
|
|
164
|
-
.query(`SELECT * FROM conversations WHERE id = ?`)
|
|
165
|
-
.get(conversationId) as Record<string, unknown>;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function makeReducerResult(overrides?: Partial<ReducerResult>): ReducerResult {
|
|
169
|
-
return {
|
|
170
|
-
timeContexts: [],
|
|
171
|
-
openLoops: [],
|
|
172
|
-
archiveObservations: [],
|
|
173
|
-
archiveEpisodes: [],
|
|
174
|
-
...overrides,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ── Teardown ──────────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
afterAll(() => {
|
|
181
|
-
resetDb();
|
|
182
|
-
try {
|
|
183
|
-
rmSync(testDir, { recursive: true });
|
|
184
|
-
} catch {
|
|
185
|
-
/* best effort */
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
beforeEach(() => {
|
|
190
|
-
resetTestTables(
|
|
191
|
-
"messages",
|
|
192
|
-
"conversations",
|
|
193
|
-
"memory_jobs",
|
|
194
|
-
"time_contexts",
|
|
195
|
-
"open_loops",
|
|
196
|
-
);
|
|
197
|
-
mockReducerResult = EMPTY_REDUCER_RESULT;
|
|
198
|
-
lastReducerInput = null;
|
|
199
|
-
reducerCallCount = 0;
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// ── Tests ─────────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
describe("findMostRecentDirtyConversation", () => {
|
|
205
|
-
test("returns the most recently updated dirty conversation", () => {
|
|
206
|
-
insertConversation("conv-old", {
|
|
207
|
-
dirtyTailMessageId: "msg-old",
|
|
208
|
-
updatedAt: NOW - 5000,
|
|
209
|
-
});
|
|
210
|
-
insertConversation("conv-recent", {
|
|
211
|
-
dirtyTailMessageId: "msg-recent",
|
|
212
|
-
updatedAt: NOW,
|
|
213
|
-
});
|
|
214
|
-
insertConversation("conv-target", { updatedAt: NOW + 1000 });
|
|
215
|
-
|
|
216
|
-
const result = findMostRecentDirtyConversation("conv-target");
|
|
217
|
-
// Should return the most recently updated dirty conversation (ordered by updatedAt DESC)
|
|
218
|
-
expect(result).toBe("conv-recent");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("excludes the target conversation", () => {
|
|
222
|
-
insertConversation("conv-dirty", {
|
|
223
|
-
dirtyTailMessageId: "msg-1",
|
|
224
|
-
updatedAt: NOW,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const result = findMostRecentDirtyConversation("conv-dirty");
|
|
228
|
-
expect(result).toBeNull();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("returns null when no dirty conversations exist", () => {
|
|
232
|
-
insertConversation("conv-clean", { updatedAt: NOW });
|
|
233
|
-
|
|
234
|
-
const result = findMostRecentDirtyConversation("conv-target");
|
|
235
|
-
expect(result).toBeNull();
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("returns null when only dirty conversation is the target", () => {
|
|
239
|
-
insertConversation("conv-target", {
|
|
240
|
-
dirtyTailMessageId: "msg-1",
|
|
241
|
-
updatedAt: NOW,
|
|
242
|
-
});
|
|
243
|
-
insertConversation("conv-clean", { updatedAt: NOW + 1000 });
|
|
244
|
-
|
|
245
|
-
const result = findMostRecentDirtyConversation("conv-target");
|
|
246
|
-
expect(result).toBeNull();
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe("reduceBeforeSwitch — conversation switch", () => {
|
|
251
|
-
test("reduces the dirty conversation before switching", async () => {
|
|
252
|
-
// Previous conversation with dirty messages
|
|
253
|
-
insertConversation("conv-prev", {
|
|
254
|
-
dirtyTailMessageId: "msg-1",
|
|
255
|
-
updatedAt: NOW,
|
|
256
|
-
});
|
|
257
|
-
insertMessage({
|
|
258
|
-
id: "msg-1",
|
|
259
|
-
conversationId: "conv-prev",
|
|
260
|
-
role: "user",
|
|
261
|
-
content: "Hello",
|
|
262
|
-
createdAt: NOW,
|
|
263
|
-
});
|
|
264
|
-
insertMessage({
|
|
265
|
-
id: "msg-2",
|
|
266
|
-
conversationId: "conv-prev",
|
|
267
|
-
role: "assistant",
|
|
268
|
-
content: "Hi there!",
|
|
269
|
-
createdAt: NOW + 1000,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// Target conversation
|
|
273
|
-
insertConversation("conv-target", { updatedAt: NOW + 5000 });
|
|
274
|
-
|
|
275
|
-
mockReducerResult = makeReducerResult({
|
|
276
|
-
openLoops: [
|
|
277
|
-
{
|
|
278
|
-
action: "create",
|
|
279
|
-
summary: "User greeted the assistant",
|
|
280
|
-
source: "conversation",
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const result = await reduceBeforeSwitch("conv-target");
|
|
286
|
-
|
|
287
|
-
// Should have reduced conv-prev
|
|
288
|
-
expect(result).toBe("conv-prev");
|
|
289
|
-
expect(reducerCallCount).toBe(1);
|
|
290
|
-
|
|
291
|
-
// Checkpoint should be advanced
|
|
292
|
-
const conv = getRawConversation("conv-prev");
|
|
293
|
-
expect(conv.memory_reduced_through_message_id).toBe("msg-2");
|
|
294
|
-
expect(conv.memory_dirty_tail_since_message_id).toBeNull();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("skips when no eligible dirty conversation exists", async () => {
|
|
298
|
-
insertConversation("conv-clean", { updatedAt: NOW });
|
|
299
|
-
insertConversation("conv-target", { updatedAt: NOW + 1000 });
|
|
300
|
-
|
|
301
|
-
const result = await reduceBeforeSwitch("conv-target");
|
|
302
|
-
|
|
303
|
-
expect(result).toBeNull();
|
|
304
|
-
expect(reducerCallCount).toBe(0);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("skips when the only dirty conversation is the target", async () => {
|
|
308
|
-
insertConversation("conv-target", {
|
|
309
|
-
dirtyTailMessageId: "msg-1",
|
|
310
|
-
updatedAt: NOW,
|
|
311
|
-
});
|
|
312
|
-
insertMessage({ id: "msg-1", conversationId: "conv-target" });
|
|
313
|
-
|
|
314
|
-
const result = await reduceBeforeSwitch("conv-target");
|
|
315
|
-
|
|
316
|
-
expect(result).toBeNull();
|
|
317
|
-
expect(reducerCallCount).toBe(0);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test("does not advance checkpoint when reducer returns empty result", async () => {
|
|
321
|
-
insertConversation("conv-prev", {
|
|
322
|
-
dirtyTailMessageId: "msg-1",
|
|
323
|
-
updatedAt: NOW,
|
|
324
|
-
});
|
|
325
|
-
insertMessage({
|
|
326
|
-
id: "msg-1",
|
|
327
|
-
conversationId: "conv-prev",
|
|
328
|
-
role: "user",
|
|
329
|
-
content: "Hello",
|
|
330
|
-
createdAt: NOW,
|
|
331
|
-
});
|
|
332
|
-
insertConversation("conv-target", { updatedAt: NOW + 5000 });
|
|
333
|
-
|
|
334
|
-
mockReducerResult = EMPTY_REDUCER_RESULT;
|
|
335
|
-
|
|
336
|
-
const result = await reduceBeforeSwitch("conv-target");
|
|
337
|
-
|
|
338
|
-
// Returns null because empty result means nothing was reduced
|
|
339
|
-
expect(result).toBeNull();
|
|
340
|
-
|
|
341
|
-
// Checkpoint should NOT advance
|
|
342
|
-
const conv = getRawConversation("conv-prev");
|
|
343
|
-
expect(conv.memory_reduced_through_message_id).toBeNull();
|
|
344
|
-
expect(conv.memory_dirty_tail_since_message_id).toBe("msg-1");
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
describe("reduceBeforeSwitch — new conversation", () => {
|
|
349
|
-
test("reduces the previous dirty conversation when starting a new one", async () => {
|
|
350
|
-
insertConversation("conv-prev", {
|
|
351
|
-
dirtyTailMessageId: "msg-1",
|
|
352
|
-
updatedAt: NOW,
|
|
353
|
-
});
|
|
354
|
-
insertMessage({
|
|
355
|
-
id: "msg-1",
|
|
356
|
-
conversationId: "conv-prev",
|
|
357
|
-
role: "user",
|
|
358
|
-
content: "Some prior work",
|
|
359
|
-
createdAt: NOW,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// The new conversation ID (just created)
|
|
363
|
-
const newConvId = "conv-new";
|
|
364
|
-
insertConversation(newConvId, { updatedAt: NOW + 5000 });
|
|
365
|
-
|
|
366
|
-
mockReducerResult = makeReducerResult({
|
|
367
|
-
timeContexts: [
|
|
368
|
-
{
|
|
369
|
-
action: "create",
|
|
370
|
-
summary: "Prior work in progress",
|
|
371
|
-
source: "conversation",
|
|
372
|
-
activeFrom: NOW,
|
|
373
|
-
activeUntil: NOW + 7 * 24 * 60 * 60 * 1000,
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const result = await reduceBeforeSwitch(newConvId);
|
|
379
|
-
|
|
380
|
-
expect(result).toBe("conv-prev");
|
|
381
|
-
expect(reducerCallCount).toBe(1);
|
|
382
|
-
|
|
383
|
-
// Verify the previous conversation's checkpoint was advanced
|
|
384
|
-
const conv = getRawConversation("conv-prev");
|
|
385
|
-
expect(conv.memory_reduced_through_message_id).toBe("msg-1");
|
|
386
|
-
expect(conv.memory_dirty_tail_since_message_id).toBeNull();
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
describe("reduceBeforeSwitch — most recent dirty selection", () => {
|
|
391
|
-
test("selects the most recently updated dirty conversation when multiple exist", async () => {
|
|
392
|
-
// Two dirty conversations — conv-newer is more recently updated
|
|
393
|
-
insertConversation("conv-older", {
|
|
394
|
-
dirtyTailMessageId: "msg-older",
|
|
395
|
-
updatedAt: NOW - 10_000,
|
|
396
|
-
});
|
|
397
|
-
insertMessage({
|
|
398
|
-
id: "msg-older",
|
|
399
|
-
conversationId: "conv-older",
|
|
400
|
-
role: "user",
|
|
401
|
-
content: "Older conversation",
|
|
402
|
-
createdAt: NOW - 10_000,
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
insertConversation("conv-newer", {
|
|
406
|
-
dirtyTailMessageId: "msg-newer",
|
|
407
|
-
updatedAt: NOW,
|
|
408
|
-
});
|
|
409
|
-
insertMessage({
|
|
410
|
-
id: "msg-newer",
|
|
411
|
-
conversationId: "conv-newer",
|
|
412
|
-
role: "user",
|
|
413
|
-
content: "Newer conversation",
|
|
414
|
-
createdAt: NOW,
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
insertConversation("conv-target", { updatedAt: NOW + 5000 });
|
|
418
|
-
|
|
419
|
-
mockReducerResult = makeReducerResult();
|
|
420
|
-
|
|
421
|
-
// Even though two are dirty, we only reduce one per switch.
|
|
422
|
-
// The function picks the most recently updated (by updatedAt DESC).
|
|
423
|
-
const result = await reduceBeforeSwitch("conv-target");
|
|
424
|
-
|
|
425
|
-
// Should pick the most recently updated dirty conversation
|
|
426
|
-
expect(result).toBe("conv-newer");
|
|
427
|
-
expect(reducerCallCount).toBe(1);
|
|
428
|
-
expect(lastReducerInput?.conversationId).toBe("conv-newer");
|
|
429
|
-
});
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
describe("reduceBeforeSwitch — error handling", () => {
|
|
433
|
-
test("returns null and continues when reducer throws", async () => {
|
|
434
|
-
insertConversation("conv-prev", {
|
|
435
|
-
dirtyTailMessageId: "msg-1",
|
|
436
|
-
updatedAt: NOW,
|
|
437
|
-
});
|
|
438
|
-
insertMessage({
|
|
439
|
-
id: "msg-1",
|
|
440
|
-
conversationId: "conv-prev",
|
|
441
|
-
role: "user",
|
|
442
|
-
content: "Hello",
|
|
443
|
-
createdAt: NOW,
|
|
444
|
-
});
|
|
445
|
-
insertConversation("conv-target", { updatedAt: NOW + 5000 });
|
|
446
|
-
|
|
447
|
-
// Override mock to throw
|
|
448
|
-
mock.module("../memory/reducer.js", () => ({
|
|
449
|
-
runReducer: async () => {
|
|
450
|
-
reducerCallCount++;
|
|
451
|
-
throw new Error("Provider timeout");
|
|
452
|
-
},
|
|
453
|
-
}));
|
|
454
|
-
|
|
455
|
-
const result = await reduceBeforeSwitch("conv-target");
|
|
456
|
-
|
|
457
|
-
// Should return null (graceful failure, don't block the switch)
|
|
458
|
-
expect(result).toBeNull();
|
|
459
|
-
|
|
460
|
-
// Checkpoint should NOT advance
|
|
461
|
-
const conv = getRawConversation("conv-prev");
|
|
462
|
-
expect(conv.memory_reduced_through_message_id).toBeNull();
|
|
463
|
-
expect(conv.memory_dirty_tail_since_message_id).toBe("msg-1");
|
|
464
|
-
|
|
465
|
-
// Restore normal mock for subsequent tests
|
|
466
|
-
mock.module("../memory/reducer.js", () => ({
|
|
467
|
-
runReducer: async (input: ReducerPromptInput) => {
|
|
468
|
-
lastReducerInput = input;
|
|
469
|
-
reducerCallCount++;
|
|
470
|
-
return mockReducerResult;
|
|
471
|
-
},
|
|
472
|
-
}));
|
|
473
|
-
});
|
|
474
|
-
});
|