@vellumai/assistant 0.4.57 → 0.5.0

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -21
  3. package/src/__tests__/encrypted-store.test.ts +24 -12
  4. package/src/__tests__/file-read-tool.test.ts +40 -0
  5. package/src/__tests__/host-file-read-tool.test.ts +87 -0
  6. package/src/__tests__/identity-intro-cache.test.ts +209 -0
  7. package/src/__tests__/model-intents.test.ts +1 -1
  8. package/src/__tests__/non-member-access-request.test.ts +3 -3
  9. package/src/__tests__/skill-memory.test.ts +14 -12
  10. package/src/daemon/conversation-runtime-assembly.ts +4 -3
  11. package/src/daemon/trace-emitter.ts +3 -2
  12. package/src/memory/search/staleness.ts +4 -1
  13. package/src/notifications/decision-engine.ts +43 -2
  14. package/src/notifications/emit-signal.ts +1 -0
  15. package/src/prompts/templates/BOOTSTRAP.md +10 -4
  16. package/src/prompts/templates/IDENTITY.md +1 -2
  17. package/src/providers/anthropic/client.ts +5 -17
  18. package/src/runtime/access-request-helper.ts +15 -1
  19. package/src/runtime/guardian-vellum-migration.ts +1 -3
  20. package/src/runtime/routes/btw-routes.ts +84 -0
  21. package/src/runtime/routes/identity-intro-cache.ts +105 -0
  22. package/src/runtime/routes/identity-routes.ts +51 -0
  23. package/src/runtime/routes/settings-routes.ts +1 -1
  24. package/src/security/encrypted-store.ts +1 -2
  25. package/src/skills/skill-memory.ts +5 -3
  26. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  27. package/src/telemetry/usage-telemetry-reporter.ts +2 -0
  28. package/src/tools/filesystem/read.ts +14 -3
  29. package/src/tools/host-filesystem/read.ts +17 -1
  30. package/src/util/pricing.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.57",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -1055,39 +1055,46 @@ describe("applyRuntimeInjections with inboundActorContext", () => {
1055
1055
 
1056
1056
  describe("buildTurnContextBlock (channel-only)", () => {
1057
1057
  test("collapses to single field when all channels match", () => {
1058
- const block = buildTurnContextBlock({
1059
- turnContext: {
1060
- userMessageChannel: "telegram",
1061
- assistantMessageChannel: "telegram",
1058
+ const block = buildTurnContextBlock(
1059
+ {
1060
+ turnContext: {
1061
+ userMessageChannel: "telegram",
1062
+ assistantMessageChannel: "telegram",
1063
+ },
1064
+ conversationOriginChannel: "telegram",
1062
1065
  },
1063
- conversationOriginChannel: "telegram",
1064
- }, undefined);
1066
+ undefined,
1067
+ );
1065
1068
  expect(block).toBe(
1066
- "<turn_context>\n" +
1067
- "channel: telegram\n" +
1068
- "</turn_context>",
1069
+ "<turn_context>\n" + "channel: telegram\n" + "</turn_context>",
1069
1070
  );
1070
1071
  });
1071
1072
 
1072
1073
  test('uses "unknown" when conversationOriginChannel is null', () => {
1073
- const block = buildTurnContextBlock({
1074
- turnContext: {
1075
- userMessageChannel: "vellum",
1076
- assistantMessageChannel: "vellum",
1074
+ const block = buildTurnContextBlock(
1075
+ {
1076
+ turnContext: {
1077
+ userMessageChannel: "vellum",
1078
+ assistantMessageChannel: "vellum",
1079
+ },
1080
+ conversationOriginChannel: null,
1077
1081
  },
1078
- conversationOriginChannel: null,
1079
- }, undefined);
1082
+ undefined,
1083
+ );
1080
1084
  expect(block).toContain("conversation_origin_channel: unknown");
1081
1085
  });
1082
1086
 
1083
1087
  test("handles mixed channels", () => {
1084
- const block = buildTurnContextBlock({
1085
- turnContext: {
1086
- userMessageChannel: "telegram",
1087
- assistantMessageChannel: "vellum",
1088
+ const block = buildTurnContextBlock(
1089
+ {
1090
+ turnContext: {
1091
+ userMessageChannel: "telegram",
1092
+ assistantMessageChannel: "vellum",
1093
+ },
1094
+ conversationOriginChannel: "vellum",
1088
1095
  },
1089
- conversationOriginChannel: "vellum",
1090
- }, undefined);
1096
+ undefined,
1097
+ );
1091
1098
  expect(block).toContain("user_message_channel: telegram");
1092
1099
  expect(block).toContain("assistant_message_channel: vellum");
1093
1100
  expect(block).toContain("conversation_origin_channel: vellum");
@@ -1,8 +1,4 @@
1
- import {
2
- createCipheriv,
3
- pbkdf2Sync,
4
- randomBytes,
5
- } from "node:crypto";
1
+ import { createCipheriv, pbkdf2Sync, randomBytes } from "node:crypto";
6
2
  import {
7
3
  chmodSync,
8
4
  existsSync,
@@ -72,11 +68,23 @@ const SALT_LENGTH = 32;
72
68
  /** Local copy of the legacy machine entropy derivation (the export was removed). */
73
69
  function legacyMachineEntropy(): string {
74
70
  const parts: string[] = [];
75
- try { parts.push(hostname()); } catch { parts.push("unknown-host"); }
76
- try { parts.push(userInfo().username); } catch { parts.push("unknown-user"); }
71
+ try {
72
+ parts.push(hostname());
73
+ } catch {
74
+ parts.push("unknown-host");
75
+ }
76
+ try {
77
+ parts.push(userInfo().username);
78
+ } catch {
79
+ parts.push("unknown-user");
80
+ }
77
81
  parts.push(process.platform);
78
82
  parts.push(process.arch);
79
- try { parts.push(userInfo().homedir); } catch { parts.push("/tmp"); }
83
+ try {
84
+ parts.push(userInfo().homedir);
85
+ } catch {
86
+ parts.push("/tmp");
87
+ }
80
88
  return parts.join(":");
81
89
  }
82
90
 
@@ -346,7 +354,8 @@ describe("encrypted-store", () => {
346
354
  // Create a v1 store using legacy encryption
347
355
  const salt = randomBytes(SALT_LENGTH);
348
356
  const legacyKey = legacyDeriveKey(salt);
349
- const entries: Record<string, { iv: string; tag: string; data: string }> = {};
357
+ const entries: Record<string, { iv: string; tag: string; data: string }> =
358
+ {};
350
359
  entries["api-key"] = legacyEncrypt("secret-123", legacyKey);
351
360
  entries["other-key"] = legacyEncrypt("secret-456", legacyKey);
352
361
  writeV1Store(STORE_PATH, salt, entries);
@@ -372,7 +381,8 @@ describe("encrypted-store", () => {
372
381
  // Create a v1 store
373
382
  const salt = randomBytes(SALT_LENGTH);
374
383
  const legacyKey = legacyDeriveKey(salt);
375
- const entries: Record<string, { iv: string; tag: string; data: string }> = {};
384
+ const entries: Record<string, { iv: string; tag: string; data: string }> =
385
+ {};
376
386
  entries["my-key"] = legacyEncrypt("my-value", legacyKey);
377
387
  writeV1Store(STORE_PATH, salt, entries);
378
388
 
@@ -393,7 +403,8 @@ describe("encrypted-store", () => {
393
403
  // Create a v1 store with one good entry and one tampered entry
394
404
  const salt = randomBytes(SALT_LENGTH);
395
405
  const legacyKey = legacyDeriveKey(salt);
396
- const entries: Record<string, { iv: string; tag: string; data: string }> = {};
406
+ const entries: Record<string, { iv: string; tag: string; data: string }> =
407
+ {};
397
408
  entries["good"] = legacyEncrypt("good-value", legacyKey);
398
409
  entries["bad"] = legacyEncrypt("bad-value", legacyKey);
399
410
  // Tamper with the bad entry
@@ -423,7 +434,8 @@ describe("encrypted-store", () => {
423
434
  // write v1 store + store.key but leave store as v1
424
435
  const salt = randomBytes(SALT_LENGTH);
425
436
  const legacyKey = legacyDeriveKey(salt);
426
- const entries: Record<string, { iv: string; tag: string; data: string }> = {};
437
+ const entries: Record<string, { iv: string; tag: string; data: string }> =
438
+ {};
427
439
  entries["test-key"] = legacyEncrypt("test-value", legacyKey);
428
440
  writeV1Store(STORE_PATH, salt, entries);
429
441
 
@@ -177,3 +177,43 @@ describe("file_read image support", () => {
177
177
  expect(result.content).toContain("outside the working directory");
178
178
  });
179
179
  });
180
+
181
+ // ── Out-of-bounds hint for host_file_read ─────────────────────────────
182
+
183
+ describe("file_read out-of-bounds hint", () => {
184
+ test("suggests host_file_read for out-of-bounds text file path", async () => {
185
+ const dir = makeTempDir();
186
+
187
+ const result = await fileReadTool.execute(
188
+ { path: "/etc/passwd" },
189
+ makeContext(dir),
190
+ );
191
+
192
+ expect(result.isError).toBe(true);
193
+ expect(result.content).toContain("host_file_read");
194
+ });
195
+
196
+ test("suggests host_file_read for out-of-bounds image file path", async () => {
197
+ const dir = makeTempDir();
198
+
199
+ const result = await fileReadTool.execute(
200
+ { path: "/Users/someone/Desktop/screenshot.png" },
201
+ makeContext(dir),
202
+ );
203
+
204
+ expect(result.isError).toBe(true);
205
+ expect(result.content).toContain("host_file_read");
206
+ });
207
+
208
+ test("does not suggest host_file_read for missing file within sandbox", async () => {
209
+ const dir = makeTempDir();
210
+
211
+ const result = await fileReadTool.execute(
212
+ { path: "nonexistent.txt" },
213
+ makeContext(dir),
214
+ );
215
+
216
+ expect(result.isError).toBe(true);
217
+ expect(result.content).not.toContain("host_file_read");
218
+ });
219
+ });
@@ -8,6 +8,12 @@ import type { ToolContext } from "../tools/types.js";
8
8
 
9
9
  const testDirs: string[] = [];
10
10
 
11
+ function makeTempDir(): string {
12
+ const dir = mkdtempSync(join(tmpdir(), "host-file-read-test-"));
13
+ testDirs.push(dir);
14
+ return dir;
15
+ }
16
+
11
17
  function makeContext(): ToolContext {
12
18
  return {
13
19
  workingDir: "/tmp",
@@ -22,6 +28,18 @@ afterEach(() => {
22
28
  }
23
29
  });
24
30
 
31
+ // Minimal valid JPEG: FF D8 FF E0 header
32
+ const JPEG_HEADER = Buffer.from([
33
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01,
34
+ 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
35
+ ]);
36
+
37
+ // Minimal PNG header
38
+ const PNG_HEADER = Buffer.from([
39
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
40
+ 0x48, 0x44, 0x52,
41
+ ]);
42
+
25
43
  describe("host_file_read tool", () => {
26
44
  test("rejects relative paths", async () => {
27
45
  const result = await hostFileReadTool.execute(
@@ -145,3 +163,72 @@ describe("host_file_read tool", () => {
145
163
  expect(result.content).toContain("symlink-content");
146
164
  });
147
165
  });
166
+
167
+ describe("host_file_read image support", () => {
168
+ test("returns image content block for .png file", async () => {
169
+ const dir = makeTempDir();
170
+ const filePath = join(dir, "screenshot.png");
171
+ writeFileSync(filePath, PNG_HEADER);
172
+
173
+ const result = await hostFileReadTool.execute(
174
+ { path: filePath },
175
+ makeContext(),
176
+ );
177
+
178
+ expect(result.isError).toBe(false);
179
+ expect(result.content).toContain("Image loaded");
180
+ expect(result.content).toContain("image/png");
181
+ expect((result as any).contentBlocks).toBeDefined();
182
+ expect((result as any).contentBlocks[0].type).toBe("image");
183
+ expect((result as any).contentBlocks[0].source.media_type).toBe(
184
+ "image/png",
185
+ );
186
+ });
187
+
188
+ test("returns correct media type for .jpg file", async () => {
189
+ const dir = makeTempDir();
190
+ const filePath = join(dir, "photo.jpg");
191
+ writeFileSync(filePath, JPEG_HEADER);
192
+
193
+ const result = await hostFileReadTool.execute(
194
+ { path: filePath },
195
+ makeContext(),
196
+ );
197
+
198
+ expect(result.isError).toBe(false);
199
+ expect(result.content).toContain("Image loaded");
200
+ expect(result.content).toContain("image/jpeg");
201
+ expect((result as any).contentBlocks).toBeDefined();
202
+ expect((result as any).contentBlocks[0].type).toBe("image");
203
+ expect((result as any).contentBlocks[0].source.media_type).toBe(
204
+ "image/jpeg",
205
+ );
206
+ });
207
+
208
+ test("returns error for non-existent image path", async () => {
209
+ const filePath = join(tmpdir(), `host-file-read-missing-${Date.now()}.png`);
210
+ const result = await hostFileReadTool.execute(
211
+ { path: filePath },
212
+ makeContext(),
213
+ );
214
+
215
+ expect(result.isError).toBe(true);
216
+ expect(result.content).toContain("file not found");
217
+ });
218
+
219
+ test("text file still works as before (regression)", async () => {
220
+ const dir = makeTempDir();
221
+ const filePath = join(dir, "notes.txt");
222
+ writeFileSync(filePath, "hello world\nsecond line\n");
223
+
224
+ const result = await hostFileReadTool.execute(
225
+ { path: filePath },
226
+ makeContext(),
227
+ );
228
+
229
+ expect(result.isError).toBe(false);
230
+ expect(result.content).toContain("1 hello world");
231
+ expect(result.content).toContain("2 second line");
232
+ expect((result as any).contentBlocks).toBeUndefined();
233
+ });
234
+ });
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Unit tests for the identity intro cache (identity-intro-cache.ts).
3
+ *
4
+ * Validates TTL-based expiration, content-hash-based invalidation when
5
+ * workspace identity files change, and round-trip get/set behavior.
6
+ */
7
+
8
+ import { afterEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Mocks — must be defined before importing the module under test
12
+ // ---------------------------------------------------------------------------
13
+
14
+ // Suppress logger output
15
+ mock.module("../util/logger.js", () => ({
16
+ getLogger: () =>
17
+ new Proxy({} as Record<string, unknown>, {
18
+ get: () => () => {},
19
+ }),
20
+ }));
21
+
22
+ // In-memory checkpoint store
23
+ const checkpointStore = new Map<string, string>();
24
+
25
+ mock.module("../memory/checkpoints.js", () => ({
26
+ getMemoryCheckpoint: (key: string) => checkpointStore.get(key) ?? null,
27
+ setMemoryCheckpoint: (key: string, value: string) => {
28
+ checkpointStore.set(key, value);
29
+ },
30
+ }));
31
+
32
+ // Simulated workspace file contents
33
+ const workspaceFiles: Record<string, string> = {};
34
+
35
+ mock.module("../util/platform.js", () => ({
36
+ getWorkspacePromptPath: (name: string) => `/mock/workspace/${name}`,
37
+ }));
38
+
39
+ mock.module("node:fs", () => ({
40
+ existsSync: (path: string) => {
41
+ const name = path.split("/").pop() ?? "";
42
+ return name in workspaceFiles;
43
+ },
44
+ readFileSync: (path: string, _encoding: string) => {
45
+ const name = path.split("/").pop() ?? "";
46
+ if (name in workspaceFiles) return workspaceFiles[name];
47
+ throw new Error(`ENOENT: ${path}`);
48
+ },
49
+ }));
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Imports (after mocks)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ import {
56
+ computeIdentityContentHash,
57
+ getCachedIntro,
58
+ setCachedIntro,
59
+ } from "../runtime/routes/identity-intro-cache.js";
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Helpers
63
+ // ---------------------------------------------------------------------------
64
+
65
+ afterEach(() => {
66
+ checkpointStore.clear();
67
+ for (const key of Object.keys(workspaceFiles)) {
68
+ delete workspaceFiles[key];
69
+ }
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Tests
74
+ // ---------------------------------------------------------------------------
75
+
76
+ describe("identity intro cache", () => {
77
+ test("returns null when cache is empty", () => {
78
+ expect(getCachedIntro()).toBeNull();
79
+ });
80
+
81
+ test("round-trip: set then get returns cached text", () => {
82
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
83
+ workspaceFiles["SOUL.md"] = "Be playful.";
84
+ workspaceFiles["USER.md"] = "The user likes coffee.";
85
+
86
+ setCachedIntro("Hey, I'm Atlas.");
87
+ const cached = getCachedIntro();
88
+ expect(cached).not.toBeNull();
89
+ expect(cached!.text).toBe("Hey, I'm Atlas.");
90
+ });
91
+
92
+ test("returns null when cache is expired (TTL exceeded)", () => {
93
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
94
+
95
+ setCachedIntro("Hello!");
96
+
97
+ // Manually set the timestamp to 5 hours ago
98
+ const fiveHoursAgo = String(Date.now() - 5 * 60 * 60 * 1000);
99
+ checkpointStore.set("identity:intro:cached_at", fiveHoursAgo);
100
+
101
+ expect(getCachedIntro()).toBeNull();
102
+ });
103
+
104
+ test("returns cached text when within TTL (3 hours ago)", () => {
105
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
106
+
107
+ setCachedIntro("Hello!");
108
+
109
+ // Set timestamp to 3 hours ago (within 4-hour TTL)
110
+ const threeHoursAgo = String(Date.now() - 3 * 60 * 60 * 1000);
111
+ checkpointStore.set("identity:intro:cached_at", threeHoursAgo);
112
+
113
+ const cached = getCachedIntro();
114
+ expect(cached).not.toBeNull();
115
+ expect(cached!.text).toBe("Hello!");
116
+ });
117
+
118
+ test("busts cache when IDENTITY.md changes", () => {
119
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
120
+ setCachedIntro("I'm Atlas!");
121
+
122
+ // Change IDENTITY.md
123
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Nova";
124
+
125
+ expect(getCachedIntro()).toBeNull();
126
+ });
127
+
128
+ test("busts cache when SOUL.md changes", () => {
129
+ workspaceFiles["SOUL.md"] = "Be playful.";
130
+ setCachedIntro("Hey there!");
131
+
132
+ // Change SOUL.md
133
+ workspaceFiles["SOUL.md"] = "Be serious and formal.";
134
+
135
+ expect(getCachedIntro()).toBeNull();
136
+ });
137
+
138
+ test("busts cache when USER.md changes", () => {
139
+ workspaceFiles["USER.md"] = "Likes coffee.";
140
+ setCachedIntro("Good morning!");
141
+
142
+ // Change USER.md
143
+ workspaceFiles["USER.md"] = "Likes tea.";
144
+
145
+ expect(getCachedIntro()).toBeNull();
146
+ });
147
+
148
+ test("cache survives when files are unchanged", () => {
149
+ workspaceFiles["IDENTITY.md"] = "- **Name:** Atlas";
150
+ workspaceFiles["SOUL.md"] = "Be chill.";
151
+ workspaceFiles["USER.md"] = "Likes sunsets.";
152
+
153
+ setCachedIntro("Atlas here.");
154
+
155
+ // Read twice — both should return the cached value
156
+ expect(getCachedIntro()?.text).toBe("Atlas here.");
157
+ expect(getCachedIntro()?.text).toBe("Atlas here.");
158
+ });
159
+
160
+ test("computeIdentityContentHash is deterministic", () => {
161
+ workspaceFiles["IDENTITY.md"] = "test";
162
+ workspaceFiles["SOUL.md"] = "test2";
163
+ workspaceFiles["USER.md"] = "test3";
164
+
165
+ const hash1 = computeIdentityContentHash();
166
+ const hash2 = computeIdentityContentHash();
167
+ expect(hash1).toBe(hash2);
168
+ expect(hash1).toMatch(/^[a-f0-9]{64}$/); // SHA-256 hex
169
+ });
170
+
171
+ test("computeIdentityContentHash changes when file content changes", () => {
172
+ workspaceFiles["IDENTITY.md"] = "v1";
173
+ const hash1 = computeIdentityContentHash();
174
+
175
+ workspaceFiles["IDENTITY.md"] = "v2";
176
+ const hash2 = computeIdentityContentHash();
177
+
178
+ expect(hash1).not.toBe(hash2);
179
+ });
180
+
181
+ test("handles missing workspace files gracefully", () => {
182
+ // No files exist — should still work (empty content hashed)
183
+ setCachedIntro("Hello!");
184
+ const cached = getCachedIntro();
185
+ expect(cached).not.toBeNull();
186
+ expect(cached!.text).toBe("Hello!");
187
+ });
188
+
189
+ test("returns null when text checkpoint is missing", () => {
190
+ checkpointStore.set("identity:intro:content_hash", "abc");
191
+ checkpointStore.set("identity:intro:cached_at", String(Date.now()));
192
+ // Missing text — should return null
193
+ expect(getCachedIntro()).toBeNull();
194
+ });
195
+
196
+ test("returns null when hash checkpoint is missing", () => {
197
+ checkpointStore.set("identity:intro:text", "Hello");
198
+ checkpointStore.set("identity:intro:cached_at", String(Date.now()));
199
+ // Missing hash — should return null
200
+ expect(getCachedIntro()).toBeNull();
201
+ });
202
+
203
+ test("returns null when timestamp checkpoint is missing", () => {
204
+ checkpointStore.set("identity:intro:text", "Hello");
205
+ checkpointStore.set("identity:intro:content_hash", "abc");
206
+ // Missing timestamp — should return null
207
+ expect(getCachedIntro()).toBeNull();
208
+ });
209
+ });
@@ -65,7 +65,7 @@ describe("model intents", () => {
65
65
  "claude-opus-4-6",
66
66
  );
67
67
  expect(resolveModelIntent("openai", "latency-optimized")).toBe(
68
- "gpt-4o-mini",
68
+ "gpt-5.4-nano",
69
69
  );
70
70
  });
71
71
 
@@ -623,7 +623,7 @@ describe("access-request-helper unit tests", () => {
623
623
  expect(telegram!.status).toBe("sent");
624
624
  });
625
625
 
626
- test("notifyGuardianOfAccessRequest records failed vellum fallback when pipeline has no vellum delivery", async () => {
626
+ test("notifyGuardianOfAccessRequest skips vellum fallback for same-channel-only routing (telegram)", async () => {
627
627
  mockEmitResult = {
628
628
  signalId: "sig-no-vellum",
629
629
  deduplicated: false,
@@ -657,8 +657,8 @@ describe("access-request-helper unit tests", () => {
657
657
  (d) => d.destinationChannel === "telegram",
658
658
  );
659
659
 
660
- expect(vellum).toBeDefined();
661
- expect(vellum!.status).toBe("failed");
660
+ // Same-channel routing skips vellum delivery entirely — no fallback record
661
+ expect(vellum).toBeUndefined();
662
662
  expect(telegram).toBeDefined();
663
663
  expect(telegram!.destinationChatId).toBe("guardian-chat-456");
664
664
  expect(telegram!.status).toBe("sent");
@@ -1,14 +1,7 @@
1
1
  import { mkdtempSync, rmSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import {
5
- afterAll,
6
- beforeEach,
7
- describe,
8
- expect,
9
- mock,
10
- test,
11
- } from "bun:test";
4
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
12
5
 
13
6
  import { eq } from "drizzle-orm";
14
7
 
@@ -46,8 +39,9 @@ mock.module("../memory/qdrant-client.js", () => ({
46
39
  }));
47
40
 
48
41
  // Controllable mock for resolveCatalog used by seedCatalogSkillMemories
49
- let mockResolveCatalog: () => Promise<import("../skills/catalog-install.js").CatalogSkill[]> =
50
- async () => [];
42
+ let mockResolveCatalog: () => Promise<
43
+ import("../skills/catalog-install.js").CatalogSkill[]
44
+ > = async () => [];
51
45
 
52
46
  mock.module("../skills/catalog-install.js", () => ({
53
47
  resolveCatalog: (..._args: unknown[]) => mockResolveCatalog(),
@@ -453,7 +447,11 @@ describe("seedCatalogSkillMemories", () => {
453
447
 
454
448
  test("skips skills whose feature flag is disabled", async () => {
455
449
  const skills: CatalogSkill[] = [
456
- makeSkill({ id: "unflagged-skill", name: "Unflagged", description: "No flag" }),
450
+ makeSkill({
451
+ id: "unflagged-skill",
452
+ name: "Unflagged",
453
+ description: "No flag",
454
+ }),
457
455
  makeSkill({
458
456
  id: "flagged-skill",
459
457
  name: "Flagged",
@@ -485,7 +483,11 @@ describe("seedCatalogSkillMemories", () => {
485
483
  test("prunes pre-existing capability for a skill whose flag becomes disabled", async () => {
486
484
  // First seed with both skills, all flags enabled
487
485
  const skills: CatalogSkill[] = [
488
- makeSkill({ id: "unflagged-skill", name: "Unflagged", description: "No flag" }),
486
+ makeSkill({
487
+ id: "unflagged-skill",
488
+ name: "Unflagged",
489
+ description: "No flag",
490
+ }),
489
491
  makeSkill({
490
492
  id: "flagged-skill",
491
493
  name: "Flagged",
@@ -655,7 +655,6 @@ export function injectTurnContext(
655
655
  };
656
656
  }
657
657
 
658
-
659
658
  /**
660
659
  * Build the `<inbound_actor_context>` text block used for model grounding.
661
660
  *
@@ -737,7 +736,10 @@ export function buildInboundActorContextBlock(
737
736
  }
738
737
  // Contact metadata - only included when the sender has a contact record
739
738
  // with non-default values.
740
- if (ctx.contactNotes && sanitizeInlineContextValue(ctx.contactNotes) !== ctx.trustClass) {
739
+ if (
740
+ ctx.contactNotes &&
741
+ sanitizeInlineContextValue(ctx.contactNotes) !== ctx.trustClass
742
+ ) {
741
743
  lines.push(
742
744
  `contact_notes: ${sanitizeInlineContextValue(ctx.contactNotes)}`,
743
745
  );
@@ -932,7 +934,6 @@ export interface InterfaceTurnContextParams {
932
934
  conversationOriginInterface: InterfaceId | null;
933
935
  }
934
936
 
935
-
936
937
  /** Strip interface turn context blocks (both legacy separate and unified). */
937
938
  export function stripInterfaceTurnContext(messages: Message[]): Message[] {
938
939
  return stripUserTextBlocksByPrefix(messages, [
@@ -67,13 +67,14 @@ export class TraceEmitter {
67
67
  attributes,
68
68
  };
69
69
 
70
+ // Send to client first so synchronous DB writes don't block SSE delivery.
71
+ this.sendToClient(event);
72
+
70
73
  try {
71
74
  persistTraceEvent(event as TraceEvent);
72
75
  } catch (err) {
73
76
  log.warn({ err, eventId }, "Failed to persist trace event");
74
77
  }
75
-
76
- this.sendToClient(event);
77
78
  }
78
79
  }
79
80
 
@@ -22,7 +22,10 @@ export function computeStaleness(
22
22
  now: number,
23
23
  ): { level: StalenessLevel; ratio: number } {
24
24
  const baseLifetime = BASE_LIFETIME_MS[item.kind] ?? DEFAULT_LIFETIME_MS;
25
- const reinforcement = Math.max(1, 1 + 0.3 * (item.sourceConversationCount - 1));
25
+ const reinforcement = Math.max(
26
+ 1,
27
+ 1 + 0.3 * (item.sourceConversationCount - 1),
28
+ );
26
29
  const effectiveLifetime = baseLifetime * reinforcement;
27
30
  const age = now - item.firstSeenAt;
28
31
  const ratio = age / effectiveLifetime;