@vellumai/assistant 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/Dockerfile +17 -27
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
  3. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/actor-token-service.test.ts +113 -0
  6. package/src/__tests__/config-schema.test.ts +2 -2
  7. package/src/__tests__/context-window-manager.test.ts +78 -0
  8. package/src/__tests__/conversation-title-service.test.ts +30 -1
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  10. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  11. package/src/__tests__/memory-regressions.test.ts +8 -30
  12. package/src/__tests__/openai-whisper.test.ts +93 -0
  13. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  14. package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
  15. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  16. package/src/__tests__/tool-executor.test.ts +4 -0
  17. package/src/__tests__/volume-security-guard.test.ts +155 -0
  18. package/src/cli/commands/conversations.ts +0 -18
  19. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
  20. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
  21. package/src/config/env-registry.ts +9 -0
  22. package/src/config/env.ts +8 -2
  23. package/src/config/feature-flag-registry.json +8 -8
  24. package/src/config/schema.ts +0 -12
  25. package/src/config/schemas/memory.ts +0 -4
  26. package/src/config/schemas/platform.ts +1 -1
  27. package/src/config/schemas/security.ts +4 -0
  28. package/src/context/window-manager.ts +53 -2
  29. package/src/credential-execution/managed-catalog.ts +5 -15
  30. package/src/daemon/conversation-agent-loop.ts +0 -60
  31. package/src/daemon/conversation-memory.ts +0 -117
  32. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  33. package/src/daemon/daemon-control.ts +7 -0
  34. package/src/daemon/handlers/conversations.ts +0 -11
  35. package/src/daemon/lifecycle.ts +10 -47
  36. package/src/daemon/providers-setup.ts +2 -1
  37. package/src/followups/followup-store.ts +5 -2
  38. package/src/hooks/manager.ts +7 -0
  39. package/src/instrument.ts +33 -1
  40. package/src/memory/conversation-crud.ts +0 -236
  41. package/src/memory/conversation-title-service.ts +26 -10
  42. package/src/memory/db-init.ts +5 -13
  43. package/src/memory/embedding-local.ts +11 -5
  44. package/src/memory/indexer.ts +15 -106
  45. package/src/memory/job-handlers/conversation-starters.ts +24 -36
  46. package/src/memory/job-handlers/embedding.ts +0 -79
  47. package/src/memory/job-utils.ts +1 -1
  48. package/src/memory/jobs-store.ts +0 -8
  49. package/src/memory/jobs-worker.ts +0 -20
  50. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  51. package/src/memory/migrations/index.ts +1 -3
  52. package/src/memory/qdrant-client.ts +4 -6
  53. package/src/memory/schema/conversations.ts +0 -3
  54. package/src/memory/schema/index.ts +0 -2
  55. package/src/messaging/draft-store.ts +2 -2
  56. package/src/messaging/provider.ts +9 -0
  57. package/src/messaging/providers/slack/adapter.ts +29 -2
  58. package/src/oauth/connection-resolver.test.ts +22 -18
  59. package/src/oauth/connection-resolver.ts +92 -7
  60. package/src/oauth/platform-connection.test.ts +78 -69
  61. package/src/oauth/platform-connection.ts +12 -19
  62. package/src/permissions/defaults.ts +3 -3
  63. package/src/permissions/trust-client.ts +332 -0
  64. package/src/permissions/trust-store-interface.ts +105 -0
  65. package/src/permissions/trust-store.ts +531 -39
  66. package/src/platform/client.test.ts +148 -0
  67. package/src/platform/client.ts +71 -0
  68. package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
  69. package/src/providers/speech-to-text/openai-whisper.ts +68 -0
  70. package/src/providers/speech-to-text/resolve.ts +9 -0
  71. package/src/providers/speech-to-text/types.ts +17 -0
  72. package/src/runtime/auth/route-policy.ts +14 -0
  73. package/src/runtime/auth/token-service.ts +133 -0
  74. package/src/runtime/http-server.ts +4 -2
  75. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  76. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  77. package/src/runtime/routes/conversation-routes.ts +2 -1
  78. package/src/runtime/routes/inbound-message-handler.ts +27 -3
  79. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
  80. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
  81. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
  82. package/src/runtime/routes/log-export-routes.ts +1 -0
  83. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  84. package/src/runtime/routes/memory-item-routes.ts +124 -2
  85. package/src/runtime/routes/secret-routes.ts +4 -1
  86. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  87. package/src/schedule/schedule-store.ts +0 -21
  88. package/src/security/ces-credential-client.ts +173 -0
  89. package/src/security/secure-keys.ts +65 -22
  90. package/src/signals/bash.ts +3 -0
  91. package/src/signals/cancel.ts +3 -0
  92. package/src/signals/confirm.ts +3 -0
  93. package/src/signals/conversation-undo.ts +3 -0
  94. package/src/signals/event-stream.ts +7 -0
  95. package/src/signals/shotgun.ts +3 -0
  96. package/src/signals/trust-rule.ts +3 -0
  97. package/src/skills/inline-command-render.ts +5 -1
  98. package/src/skills/inline-command-runner.ts +30 -2
  99. package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
  100. package/src/telemetry/usage-telemetry-reporter.ts +21 -19
  101. package/src/tools/memory/handlers.ts +1 -129
  102. package/src/tools/permission-checker.ts +18 -0
  103. package/src/tools/skills/load.ts +9 -2
  104. package/src/util/device-id.ts +70 -7
  105. package/src/util/logger.ts +35 -9
  106. package/src/util/platform.ts +29 -5
  107. package/src/util/xml.ts +8 -0
  108. package/src/workspace/heartbeat-service.ts +5 -24
  109. package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
  110. package/src/workspace/migrations/registry.ts +2 -0
  111. package/src/__tests__/archive-recall.test.ts +0 -560
  112. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  113. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  114. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  115. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  116. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  117. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  118. package/src/__tests__/memory-brief-time.test.ts +0 -285
  119. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  120. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  121. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  122. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  123. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  124. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  125. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  126. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  127. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  128. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  129. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  130. package/src/__tests__/memory-reducer.test.ts +0 -704
  131. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  132. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  133. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  134. package/src/config/schemas/memory-simplified.ts +0 -101
  135. package/src/memory/archive-recall.ts +0 -516
  136. package/src/memory/archive-store.ts +0 -400
  137. package/src/memory/brief-formatting.ts +0 -33
  138. package/src/memory/brief-open-loops.ts +0 -266
  139. package/src/memory/brief-time.ts +0 -162
  140. package/src/memory/brief.ts +0 -75
  141. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  142. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  143. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  144. package/src/memory/migrations/186-memory-archive.ts +0 -109
  145. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  146. package/src/memory/reducer-scheduler.ts +0 -242
  147. package/src/memory/reducer-store.ts +0 -271
  148. package/src/memory/reducer-types.ts +0 -106
  149. package/src/memory/reducer.ts +0 -467
  150. package/src/memory/schema/memory-archive.ts +0 -121
  151. package/src/memory/schema/memory-brief.ts +0 -55
@@ -1,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
- });