@vellumai/assistant 0.5.3 → 0.5.5
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 +18 -27
- package/docs/architecture/memory.md +105 -0
- 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__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-types.test.ts +12 -4
- package/src/__tests__/memory-reducer.test.ts +7 -1
- package/src/__tests__/memory-regressions.test.ts +24 -4
- package/src/__tests__/memory-simplified-config.test.ts +4 -4
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +0 -1
- package/src/config/schemas/memory-simplified.ts +1 -1
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/config-watcher.ts +4 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/lifecycle.ts +51 -2
- package/src/daemon/providers-setup.ts +2 -1
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/brief-time.ts +5 -4
- package/src/memory/conversation-crud.ts +210 -0
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +24 -30
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/jobs-store.ts +2 -0
- package/src/memory/jobs-worker.ts +8 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-types.ts +9 -2
- package/src/memory/reducer.ts +25 -11
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/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/trust-client.ts +343 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +523 -36
- 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 +10 -1
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +88 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- 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/secret-routes.ts +5 -1
- package/src/schedule/schedule-store.ts +7 -0
- package/src/schedule/scheduler.ts +6 -2
- 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/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +22 -20
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -0,0 +1,474 @@
|
|
|
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
|
+
});
|
|
@@ -222,6 +222,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
222
222
|
"messaging/providers/telegram-bot/adapter.ts", // Telegram bot token lookup for connectivity check
|
|
223
223
|
"runtime/channel-readiness-service.ts", // channel readiness probes for Telegram connectivity
|
|
224
224
|
"messaging/providers/whatsapp/adapter.ts", // WhatsApp credential lookup for connectivity check
|
|
225
|
+
"messaging/providers/slack/adapter.ts", // Slack bot token lookup for Socket Mode connectivity check
|
|
225
226
|
"daemon/handlers/config-slack-channel.ts", // Slack channel config credential management
|
|
226
227
|
"providers/managed-proxy/context.ts", // managed proxy API key lookup for provider initialization
|
|
227
228
|
"mcp/mcp-oauth-provider.ts", // MCP OAuth token/client/discovery persistence
|
|
@@ -254,6 +255,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
254
255
|
"config/bundled-skills/slack/tools/shared.ts", // Slack skill bot token lookup
|
|
255
256
|
"daemon/conversation-process.ts", // masked provider key display
|
|
256
257
|
"daemon/handlers/config-model.ts", // masked provider key display
|
|
258
|
+
"providers/speech-to-text/resolve.ts", // STT provider API key lookup
|
|
257
259
|
]);
|
|
258
260
|
|
|
259
261
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,6 +41,7 @@ describe("schedule_syntax column migration", () => {
|
|
|
41
41
|
routing_intent TEXT NOT NULL DEFAULT 'all_channels',
|
|
42
42
|
routing_hints_json TEXT NOT NULL DEFAULT '{}',
|
|
43
43
|
status TEXT NOT NULL DEFAULT 'active',
|
|
44
|
+
quiet INTEGER NOT NULL DEFAULT 0,
|
|
44
45
|
created_at INTEGER NOT NULL,
|
|
45
46
|
updated_at INTEGER NOT NULL
|
|
46
47
|
)
|
|
@@ -96,6 +97,7 @@ describe("schedule_syntax column migration", () => {
|
|
|
96
97
|
routing_intent TEXT NOT NULL DEFAULT 'all_channels',
|
|
97
98
|
routing_hints_json TEXT NOT NULL DEFAULT '{}',
|
|
98
99
|
status TEXT NOT NULL DEFAULT 'active',
|
|
100
|
+
quiet INTEGER NOT NULL DEFAULT 0,
|
|
99
101
|
created_at INTEGER NOT NULL,
|
|
100
102
|
updated_at INTEGER NOT NULL
|
|
101
103
|
)
|
|
@@ -145,6 +147,7 @@ describe("schedule_syntax column migration", () => {
|
|
|
145
147
|
routing_intent TEXT NOT NULL DEFAULT 'all_channels',
|
|
146
148
|
routing_hints_json TEXT NOT NULL DEFAULT '{}',
|
|
147
149
|
status TEXT NOT NULL DEFAULT 'active',
|
|
150
|
+
quiet INTEGER NOT NULL DEFAULT 0,
|
|
148
151
|
created_at INTEGER NOT NULL,
|
|
149
152
|
updated_at INTEGER NOT NULL
|
|
150
153
|
)
|