@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,285 +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(), "brief-time-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
|
-
truncateForLog: (value: string) => value,
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
import { compileTimeBrief } from "../memory/brief-time.js";
|
|
28
|
-
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
29
|
-
import { createSchedule } from "../schedule/schedule-store.js";
|
|
30
|
-
|
|
31
|
-
initializeDb();
|
|
32
|
-
|
|
33
|
-
const SCOPE_ID = "default";
|
|
34
|
-
const HOUR = 60 * 60 * 1000;
|
|
35
|
-
const DAY = 24 * HOUR;
|
|
36
|
-
|
|
37
|
-
function getRawDb(): import("bun:sqlite").Database {
|
|
38
|
-
return (getDb() as unknown as { $client: import("bun:sqlite").Database })
|
|
39
|
-
.$client;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function insertTimeContext(opts: {
|
|
43
|
-
id: string;
|
|
44
|
-
summary: string;
|
|
45
|
-
source?: string;
|
|
46
|
-
activeFrom: number;
|
|
47
|
-
activeUntil: number;
|
|
48
|
-
scopeId?: string;
|
|
49
|
-
}): void {
|
|
50
|
-
const now = Date.now();
|
|
51
|
-
getRawDb().run(
|
|
52
|
-
`INSERT INTO time_contexts (id, scope_id, summary, source, active_from, active_until, created_at, updated_at)
|
|
53
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
54
|
-
[
|
|
55
|
-
opts.id,
|
|
56
|
-
opts.scopeId ?? SCOPE_ID,
|
|
57
|
-
opts.summary,
|
|
58
|
-
opts.source ?? "conversation",
|
|
59
|
-
opts.activeFrom,
|
|
60
|
-
opts.activeUntil,
|
|
61
|
-
now,
|
|
62
|
-
now,
|
|
63
|
-
],
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
afterAll(() => {
|
|
68
|
-
resetDb();
|
|
69
|
-
try {
|
|
70
|
-
rmSync(testDir, { recursive: true });
|
|
71
|
-
} catch {
|
|
72
|
-
/* best effort */
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
getRawDb().run("DELETE FROM time_contexts");
|
|
78
|
-
getRawDb().run("DELETE FROM cron_runs");
|
|
79
|
-
getRawDb().run("DELETE FROM cron_jobs");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// ────────────────────────────────────────────────────────────────────
|
|
83
|
-
// Tests
|
|
84
|
-
// ────────────────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
describe("compileTimeBrief", () => {
|
|
87
|
-
test("returns null when nothing qualifies", () => {
|
|
88
|
-
const now = Date.now();
|
|
89
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
90
|
-
expect(result).toBeNull();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("surfaces a tomorrow-morning event from time_contexts", () => {
|
|
94
|
-
const now = Date.now();
|
|
95
|
-
// Active window that starts before now and ends tomorrow
|
|
96
|
-
insertTimeContext({
|
|
97
|
-
id: "tc-morning",
|
|
98
|
-
summary: "Team standup tomorrow at 9am",
|
|
99
|
-
activeFrom: now - HOUR,
|
|
100
|
-
activeUntil: now + DAY,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
104
|
-
expect(result).not.toBeNull();
|
|
105
|
-
expect(result).toContain("### Time-Relevant Context");
|
|
106
|
-
expect(result).toContain("Team standup tomorrow at 9am");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("surfaces a temporary situation (currently happening)", () => {
|
|
110
|
-
const now = Date.now();
|
|
111
|
-
// Active for the next 2 hours
|
|
112
|
-
insertTimeContext({
|
|
113
|
-
id: "tc-situation",
|
|
114
|
-
summary: "User is in a meeting until 3pm",
|
|
115
|
-
activeFrom: now - 30 * 60 * 1000,
|
|
116
|
-
activeUntil: now + 2 * HOUR,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
120
|
-
expect(result).not.toBeNull();
|
|
121
|
-
expect(result).toContain("User is in a meeting until 3pm");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("expired time_contexts are not surfaced", () => {
|
|
125
|
-
const now = Date.now();
|
|
126
|
-
// Expired yesterday
|
|
127
|
-
insertTimeContext({
|
|
128
|
-
id: "tc-expired",
|
|
129
|
-
summary: "Dentist appointment yesterday",
|
|
130
|
-
activeFrom: now - 2 * DAY,
|
|
131
|
-
activeUntil: now - DAY,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
135
|
-
expect(result).toBeNull();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("future time_contexts not yet active are not surfaced", () => {
|
|
139
|
-
const now = Date.now();
|
|
140
|
-
// Starts tomorrow
|
|
141
|
-
insertTimeContext({
|
|
142
|
-
id: "tc-future",
|
|
143
|
-
summary: "Vacation starts next week",
|
|
144
|
-
activeFrom: now + DAY,
|
|
145
|
-
activeUntil: now + 8 * DAY,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
149
|
-
expect(result).toBeNull();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("includes due-soon schedule jobs", () => {
|
|
153
|
-
const now = Date.now();
|
|
154
|
-
// Create a one-shot schedule due in 2 hours
|
|
155
|
-
createSchedule({
|
|
156
|
-
name: "Send weekly report",
|
|
157
|
-
message: "Time to send the weekly report",
|
|
158
|
-
nextRunAt: now + 2 * HOUR,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
162
|
-
expect(result).not.toBeNull();
|
|
163
|
-
expect(result).toContain("### Time-Relevant Context");
|
|
164
|
-
expect(result).toContain('Scheduled: "Send weekly report"');
|
|
165
|
-
expect(result).toContain("in 2 hours");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("sorts by urgency: happening now > overdue > within 24h > within 7d", () => {
|
|
169
|
-
const now = Date.now();
|
|
170
|
-
|
|
171
|
-
// Within 7 days (lower priority)
|
|
172
|
-
insertTimeContext({
|
|
173
|
-
id: "tc-week",
|
|
174
|
-
summary: "Quarterly review ends Friday",
|
|
175
|
-
activeFrom: now - DAY,
|
|
176
|
-
activeUntil: now + 5 * DAY,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Happening now (expiring in 6 hours — highest priority)
|
|
180
|
-
insertTimeContext({
|
|
181
|
-
id: "tc-now",
|
|
182
|
-
summary: "User traveling today",
|
|
183
|
-
activeFrom: now - 2 * HOUR,
|
|
184
|
-
activeUntil: now + 6 * HOUR,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Within 24h (ending tomorrow — medium priority)
|
|
188
|
-
insertTimeContext({
|
|
189
|
-
id: "tc-24h",
|
|
190
|
-
summary: "Project deadline tomorrow morning",
|
|
191
|
-
activeFrom: now - DAY,
|
|
192
|
-
activeUntil: now + 20 * HOUR,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
196
|
-
expect(result).not.toBeNull();
|
|
197
|
-
|
|
198
|
-
const lines = result!.split("\n").filter((l) => l.startsWith("- "));
|
|
199
|
-
expect(lines.length).toBe(3);
|
|
200
|
-
// Happening now (remaining <= 24h) comes first
|
|
201
|
-
expect(lines[0]).toContain("User traveling today");
|
|
202
|
-
// Within 24h comes second
|
|
203
|
-
expect(lines[1]).toContain("Project deadline tomorrow morning");
|
|
204
|
-
// Within 7d comes last
|
|
205
|
-
expect(lines[2]).toContain("Quarterly review ends Friday");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("caps at 3 entries", () => {
|
|
209
|
-
const now = Date.now();
|
|
210
|
-
|
|
211
|
-
insertTimeContext({
|
|
212
|
-
id: "tc-1",
|
|
213
|
-
summary: "Context one",
|
|
214
|
-
activeFrom: now - HOUR,
|
|
215
|
-
activeUntil: now + 2 * HOUR,
|
|
216
|
-
});
|
|
217
|
-
insertTimeContext({
|
|
218
|
-
id: "tc-2",
|
|
219
|
-
summary: "Context two",
|
|
220
|
-
activeFrom: now - HOUR,
|
|
221
|
-
activeUntil: now + 3 * HOUR,
|
|
222
|
-
});
|
|
223
|
-
insertTimeContext({
|
|
224
|
-
id: "tc-3",
|
|
225
|
-
summary: "Context three",
|
|
226
|
-
activeFrom: now - HOUR,
|
|
227
|
-
activeUntil: now + 4 * HOUR,
|
|
228
|
-
});
|
|
229
|
-
insertTimeContext({
|
|
230
|
-
id: "tc-4",
|
|
231
|
-
summary: "Context four (should be dropped)",
|
|
232
|
-
activeFrom: now - HOUR,
|
|
233
|
-
activeUntil: now + 5 * HOUR,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
237
|
-
expect(result).not.toBeNull();
|
|
238
|
-
|
|
239
|
-
const lines = result!.split("\n").filter((l) => l.startsWith("- "));
|
|
240
|
-
expect(lines.length).toBe(3);
|
|
241
|
-
expect(result).not.toContain("Context four");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test("filters by scopeId — ignores other scopes", () => {
|
|
245
|
-
const now = Date.now();
|
|
246
|
-
insertTimeContext({
|
|
247
|
-
id: "tc-other",
|
|
248
|
-
summary: "Other scope context",
|
|
249
|
-
activeFrom: now - HOUR,
|
|
250
|
-
activeUntil: now + DAY,
|
|
251
|
-
scopeId: "other-scope",
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
255
|
-
expect(result).toBeNull();
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("mixes time_contexts and schedules in deterministic order", () => {
|
|
259
|
-
const now = Date.now();
|
|
260
|
-
|
|
261
|
-
// A schedule due in 30 minutes (within 24h bucket)
|
|
262
|
-
createSchedule({
|
|
263
|
-
name: "Daily standup reminder",
|
|
264
|
-
message: "Standup time",
|
|
265
|
-
nextRunAt: now + 30 * 60 * 1000,
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// A time context happening now (remaining 3 hours)
|
|
269
|
-
insertTimeContext({
|
|
270
|
-
id: "tc-active",
|
|
271
|
-
summary: "Focus time until noon",
|
|
272
|
-
activeFrom: now - HOUR,
|
|
273
|
-
activeUntil: now + 3 * HOUR,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const result = compileTimeBrief(getDb(), SCOPE_ID, now);
|
|
277
|
-
expect(result).not.toBeNull();
|
|
278
|
-
|
|
279
|
-
const lines = result!.split("\n").filter((l) => l.startsWith("- "));
|
|
280
|
-
expect(lines.length).toBe(2);
|
|
281
|
-
// Both happening-now time context and within-24h schedule should appear
|
|
282
|
-
expect(result).toContain("Focus time until noon");
|
|
283
|
-
expect(result).toContain("Daily standup reminder");
|
|
284
|
-
});
|
|
285
|
-
});
|
|
@@ -1,311 +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(), "brief-wrapper-test-"));
|
|
7
|
-
|
|
8
|
-
mock.module("../util/platform.js", () => ({
|
|
9
|
-
getDataDir: () => testDir,
|
|
10
|
-
getRootDir: () => testDir,
|
|
11
|
-
isMacOS: () => process.platform === "darwin",
|
|
12
|
-
isLinux: () => process.platform === "linux",
|
|
13
|
-
isWindows: () => process.platform === "win32",
|
|
14
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
15
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
16
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
17
|
-
ensureDataDir: () => {},
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
mock.module("../util/logger.js", () => ({
|
|
21
|
-
getLogger: () =>
|
|
22
|
-
new Proxy({} as Record<string, unknown>, {
|
|
23
|
-
get: () => () => {},
|
|
24
|
-
}),
|
|
25
|
-
truncateForLog: (value: string) => value,
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
stripInjectedContext,
|
|
30
|
-
stripUserTextBlocksByPrefix,
|
|
31
|
-
} from "../daemon/conversation-runtime-assembly.js";
|
|
32
|
-
import { compileMemoryBrief } from "../memory/brief.js";
|
|
33
|
-
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
34
|
-
import { getSqlite } from "../memory/db-connection.js";
|
|
35
|
-
import { resetTestTables } from "../memory/raw-query.js";
|
|
36
|
-
import type { Message } from "../providers/types.js";
|
|
37
|
-
|
|
38
|
-
initializeDb();
|
|
39
|
-
|
|
40
|
-
// ── Constants ──────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
const SCOPE_ID = "default";
|
|
43
|
-
const HOUR = 60 * 60 * 1000;
|
|
44
|
-
const DAY = 24 * HOUR;
|
|
45
|
-
|
|
46
|
-
// ── Helpers ────────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
function getRawDb(): import("bun:sqlite").Database {
|
|
49
|
-
return getSqlite();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function insertTimeContext(opts: {
|
|
53
|
-
id: string;
|
|
54
|
-
summary: string;
|
|
55
|
-
activeFrom: number;
|
|
56
|
-
activeUntil: number;
|
|
57
|
-
scopeId?: string;
|
|
58
|
-
}): void {
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
getRawDb().run(
|
|
61
|
-
`INSERT INTO time_contexts (id, scope_id, summary, source, active_from, active_until, created_at, updated_at)
|
|
62
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
63
|
-
[
|
|
64
|
-
opts.id,
|
|
65
|
-
opts.scopeId ?? SCOPE_ID,
|
|
66
|
-
opts.summary,
|
|
67
|
-
"conversation",
|
|
68
|
-
opts.activeFrom,
|
|
69
|
-
opts.activeUntil,
|
|
70
|
-
now,
|
|
71
|
-
now,
|
|
72
|
-
],
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function insertOpenLoop(opts: {
|
|
77
|
-
id: string;
|
|
78
|
-
summary: string;
|
|
79
|
-
dueAt?: number | null;
|
|
80
|
-
updatedAt?: number;
|
|
81
|
-
}): void {
|
|
82
|
-
const now = Date.now();
|
|
83
|
-
getRawDb().run(
|
|
84
|
-
`INSERT INTO open_loops (id, scope_id, summary, status, source, due_at, surfaced_at, created_at, updated_at)
|
|
85
|
-
VALUES (?, ?, ?, ?, 'conversation', ?, ?, ?, ?)`,
|
|
86
|
-
[
|
|
87
|
-
opts.id,
|
|
88
|
-
SCOPE_ID,
|
|
89
|
-
opts.summary,
|
|
90
|
-
"open",
|
|
91
|
-
opts.dueAt ?? null,
|
|
92
|
-
null,
|
|
93
|
-
now,
|
|
94
|
-
opts.updatedAt ?? now,
|
|
95
|
-
],
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── Teardown ───────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
afterAll(() => {
|
|
102
|
-
resetDb();
|
|
103
|
-
try {
|
|
104
|
-
rmSync(testDir, { recursive: true });
|
|
105
|
-
} catch {
|
|
106
|
-
/* best effort */
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
beforeEach(() => {
|
|
111
|
-
resetTestTables(
|
|
112
|
-
"time_contexts",
|
|
113
|
-
"open_loops",
|
|
114
|
-
"work_items",
|
|
115
|
-
"tasks",
|
|
116
|
-
"task_runs",
|
|
117
|
-
"followups",
|
|
118
|
-
"cron_runs",
|
|
119
|
-
"cron_jobs",
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// ── Tests ──────────────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
describe("compileMemoryBrief", () => {
|
|
126
|
-
test("returns empty string when neither section has content", () => {
|
|
127
|
-
const now = Date.now();
|
|
128
|
-
const result = compileMemoryBrief(getDb(), SCOPE_ID, "msg-1", now);
|
|
129
|
-
expect(result.text).toBe("");
|
|
130
|
-
expect(result.resurfacedLoopId).toBeNull();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("renders only the time section when open loops are empty", () => {
|
|
134
|
-
const now = Date.now();
|
|
135
|
-
insertTimeContext({
|
|
136
|
-
id: "tc-1",
|
|
137
|
-
summary: "Meeting with Alice in 2 hours",
|
|
138
|
-
activeFrom: now - HOUR,
|
|
139
|
-
activeUntil: now + 2 * HOUR,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const result = compileMemoryBrief(getDb(), SCOPE_ID, "msg-1", now);
|
|
143
|
-
|
|
144
|
-
expect(result.text).toContain("<memory_brief>");
|
|
145
|
-
expect(result.text).toContain("</memory_brief>");
|
|
146
|
-
expect(result.text).toContain("### Time-Relevant Context");
|
|
147
|
-
expect(result.text).toContain("Meeting with Alice in 2 hours");
|
|
148
|
-
// Should NOT contain Open Loops section
|
|
149
|
-
expect(result.text).not.toContain("### Open Loops");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("renders only the open loops section when time context is empty", () => {
|
|
153
|
-
const now = Date.now();
|
|
154
|
-
insertOpenLoop({
|
|
155
|
-
id: "ol-1",
|
|
156
|
-
summary: "Fix the login bug",
|
|
157
|
-
dueAt: now + 12 * HOUR,
|
|
158
|
-
updatedAt: now - DAY * 10,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const result = compileMemoryBrief(getDb(), SCOPE_ID, "msg-1", now);
|
|
162
|
-
|
|
163
|
-
expect(result.text).toContain("<memory_brief>");
|
|
164
|
-
expect(result.text).toContain("</memory_brief>");
|
|
165
|
-
expect(result.text).toContain("### Open Loops");
|
|
166
|
-
expect(result.text).toContain("Fix the login bug");
|
|
167
|
-
// Should NOT contain Time-Relevant Context section
|
|
168
|
-
expect(result.text).not.toContain("### Time-Relevant Context");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("renders both sections when both have content", () => {
|
|
172
|
-
const now = Date.now();
|
|
173
|
-
|
|
174
|
-
insertTimeContext({
|
|
175
|
-
id: "tc-1",
|
|
176
|
-
summary: "Quarterly review deadline tomorrow",
|
|
177
|
-
activeFrom: now - HOUR,
|
|
178
|
-
activeUntil: now + DAY,
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
insertOpenLoop({
|
|
182
|
-
id: "ol-1",
|
|
183
|
-
summary: "Reply to vendor email",
|
|
184
|
-
dueAt: now + 6 * HOUR,
|
|
185
|
-
updatedAt: now - DAY * 10,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const result = compileMemoryBrief(getDb(), SCOPE_ID, "msg-1", now);
|
|
189
|
-
|
|
190
|
-
expect(result.text).toContain("<memory_brief>");
|
|
191
|
-
expect(result.text).toContain("</memory_brief>");
|
|
192
|
-
expect(result.text).toContain("### Time-Relevant Context");
|
|
193
|
-
expect(result.text).toContain("Quarterly review deadline tomorrow");
|
|
194
|
-
expect(result.text).toContain("### Open Loops");
|
|
195
|
-
expect(result.text).toContain("Reply to vendor email");
|
|
196
|
-
|
|
197
|
-
// Sections should be separated by a blank line
|
|
198
|
-
expect(result.text).toContain(
|
|
199
|
-
"### Time-Relevant Context\n- Quarterly review deadline tomorrow\n\n### Open Loops",
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("wraps content in <memory_brief> tags", () => {
|
|
204
|
-
const now = Date.now();
|
|
205
|
-
insertTimeContext({
|
|
206
|
-
id: "tc-1",
|
|
207
|
-
summary: "Something happening",
|
|
208
|
-
activeFrom: now - HOUR,
|
|
209
|
-
activeUntil: now + HOUR,
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const result = compileMemoryBrief(getDb(), SCOPE_ID, "msg-1", now);
|
|
213
|
-
|
|
214
|
-
expect(result.text.startsWith("<memory_brief>\n")).toBe(true);
|
|
215
|
-
expect(result.text.endsWith("\n</memory_brief>")).toBe(true);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("forwards resurfacedLoopId from open-loop compiler", () => {
|
|
219
|
-
const now = Date.now();
|
|
220
|
-
// Insert a low-salience loop (no dueAt, updated long ago) to trigger resurfacing
|
|
221
|
-
insertOpenLoop({
|
|
222
|
-
id: "ol-stale",
|
|
223
|
-
summary: "Old forgotten task",
|
|
224
|
-
updatedAt: now - DAY * 30,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const result = compileMemoryBrief(getDb(), SCOPE_ID, "msg-1", now);
|
|
228
|
-
// The stale loop should be resurfaced
|
|
229
|
-
expect(result.resurfacedLoopId).toBe("ol-stale");
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// ── Strip tests ────────────────────────────────────────────────────────
|
|
234
|
-
|
|
235
|
-
describe("memory_brief strip support", () => {
|
|
236
|
-
test("stripInjectedContext removes <memory_brief> blocks", () => {
|
|
237
|
-
const briefText = `<memory_brief>\n### Time-Relevant Context\n- Meeting in 2 hours\n</memory_brief>`;
|
|
238
|
-
const messages: Message[] = [
|
|
239
|
-
{
|
|
240
|
-
role: "user",
|
|
241
|
-
content: [
|
|
242
|
-
{ type: "text", text: briefText },
|
|
243
|
-
{ type: "text", text: "What's on my calendar?" },
|
|
244
|
-
],
|
|
245
|
-
},
|
|
246
|
-
];
|
|
247
|
-
|
|
248
|
-
const stripped = stripInjectedContext(messages);
|
|
249
|
-
expect(stripped).toHaveLength(1);
|
|
250
|
-
expect(stripped[0].content).toHaveLength(1);
|
|
251
|
-
expect(stripped[0].content[0]).toEqual({
|
|
252
|
-
type: "text",
|
|
253
|
-
text: "What's on my calendar?",
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("stripUserTextBlocksByPrefix removes <memory_brief> by prefix", () => {
|
|
258
|
-
const briefText = `<memory_brief>\n### Open Loops\n- Fix the bug\n</memory_brief>`;
|
|
259
|
-
const messages: Message[] = [
|
|
260
|
-
{
|
|
261
|
-
role: "user",
|
|
262
|
-
content: [
|
|
263
|
-
{ type: "text", text: briefText },
|
|
264
|
-
{ type: "text", text: "Hello" },
|
|
265
|
-
],
|
|
266
|
-
},
|
|
267
|
-
];
|
|
268
|
-
|
|
269
|
-
const stripped = stripUserTextBlocksByPrefix(messages, ["<memory_brief>"]);
|
|
270
|
-
expect(stripped).toHaveLength(1);
|
|
271
|
-
expect(stripped[0].content).toHaveLength(1);
|
|
272
|
-
expect(stripped[0].content[0]).toEqual({ type: "text", text: "Hello" });
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test("drops entire message when only a <memory_brief> block remains", () => {
|
|
276
|
-
const briefText = `<memory_brief>\n### Time-Relevant Context\n- Deadline today\n</memory_brief>`;
|
|
277
|
-
const messages: Message[] = [
|
|
278
|
-
{
|
|
279
|
-
role: "user",
|
|
280
|
-
content: [{ type: "text", text: briefText }],
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
role: "assistant",
|
|
284
|
-
content: [{ type: "text", text: "Got it." }],
|
|
285
|
-
},
|
|
286
|
-
];
|
|
287
|
-
|
|
288
|
-
const stripped = stripInjectedContext(messages);
|
|
289
|
-
// The user message with only the brief block should be dropped entirely
|
|
290
|
-
expect(stripped).toHaveLength(1);
|
|
291
|
-
expect(stripped[0].role).toBe("assistant");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("preserves user-authored text that does not start with <memory_brief>", () => {
|
|
295
|
-
const messages: Message[] = [
|
|
296
|
-
{
|
|
297
|
-
role: "user",
|
|
298
|
-
content: [
|
|
299
|
-
{ type: "text", text: "I was thinking about <memory_brief> tags" },
|
|
300
|
-
],
|
|
301
|
-
},
|
|
302
|
-
];
|
|
303
|
-
|
|
304
|
-
const stripped = stripInjectedContext(messages);
|
|
305
|
-
expect(stripped).toHaveLength(1);
|
|
306
|
-
expect(stripped[0].content[0]).toEqual({
|
|
307
|
-
type: "text",
|
|
308
|
-
text: "I was thinking about <memory_brief> tags",
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
});
|