lynkr 3.0.0 → 3.2.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.
@@ -0,0 +1,105 @@
1
+ const logger = require("../logger");
2
+ const config = require("../config");
3
+
4
+ const TRUNCATION_LIMITS = {
5
+ Read: { maxChars: 8000, strategy: 'middle' },
6
+ Bash: { maxChars: 30000, strategy: 'tail' },
7
+ Grep: { maxChars: 12000, strategy: 'head' },
8
+ Glob: { maxChars: 8000, strategy: 'head' },
9
+ WebFetch: { maxChars: 16000, strategy: 'head' },
10
+ WebSearch: { maxChars: 12000, strategy: 'head' },
11
+ LSP: { maxChars: 8000, strategy: 'head' },
12
+ Edit: { maxChars: 8000, strategy: 'middle' },
13
+ Write: { maxChars: 8000, strategy: 'middle' },
14
+ Task: { maxChars: 20000, strategy: 'tail' },
15
+ AgentTask: { maxChars: 20000, strategy: 'tail' },
16
+ };
17
+
18
+ /**
19
+ * Apply truncation strategy to text
20
+ */
21
+ function applyTruncationStrategy(text, maxChars, strategy) {
22
+ if (text.length <= maxChars) {
23
+ return text;
24
+ }
25
+
26
+ switch (strategy) {
27
+ case 'head':
28
+ // Keep beginning
29
+ return text.slice(0, maxChars);
30
+
31
+ case 'tail':
32
+ // Keep end
33
+ return text.slice(-maxChars);
34
+
35
+ case 'middle': {
36
+ // Keep start and end, remove middle
37
+ const keepSize = Math.floor(maxChars / 2);
38
+ const start = text.slice(0, keepSize);
39
+ const end = text.slice(-keepSize);
40
+ const removed = text.length - (keepSize * 2);
41
+ return `${start}\n\n... [${removed} characters truncated for token efficiency] ...\n\n${end}`;
42
+ }
43
+
44
+ default:
45
+ return text.slice(0, maxChars);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Truncate tool output based on tool type
51
+ */
52
+ function truncateToolOutput(toolName, output) {
53
+ // Skip if truncation disabled
54
+ if (config.toolTruncation?.enabled === false) {
55
+ return output;
56
+ }
57
+
58
+ if (!output || typeof output !== 'string') {
59
+ return output;
60
+ }
61
+
62
+ const limit = TRUNCATION_LIMITS[toolName];
63
+ if (!limit) {
64
+ // No truncation for unknown tools
65
+ return output;
66
+ }
67
+
68
+ if (output.length <= limit.maxChars) {
69
+ return output;
70
+ }
71
+
72
+ const truncated = applyTruncationStrategy(output, limit.maxChars, limit.strategy);
73
+ const removed = output.length - truncated.length;
74
+
75
+ logger.debug({
76
+ tool: toolName,
77
+ originalLength: output.length,
78
+ truncatedLength: truncated.length,
79
+ removed,
80
+ strategy: limit.strategy
81
+ }, 'Truncated tool output for token efficiency');
82
+
83
+ return truncated;
84
+ }
85
+
86
+ /**
87
+ * Get truncation limit for a specific tool
88
+ */
89
+ function getTruncationLimit(toolName) {
90
+ return TRUNCATION_LIMITS[toolName] || null;
91
+ }
92
+
93
+ /**
94
+ * Update truncation limit for a tool (useful for testing)
95
+ */
96
+ function setTruncationLimit(toolName, maxChars, strategy = 'head') {
97
+ TRUNCATION_LIMITS[toolName] = { maxChars, strategy };
98
+ }
99
+
100
+ module.exports = {
101
+ truncateToolOutput,
102
+ getTruncationLimit,
103
+ setTruncationLimit,
104
+ TRUNCATION_LIMITS
105
+ };
@@ -0,0 +1,217 @@
1
+ const logger = require("../logger");
2
+
3
+ /**
4
+ * Estimate token count (rough approximation: 4 chars ≈ 1 token)
5
+ * For production, consider using @anthropic-ai/tokenizer for exact counts
6
+ */
7
+ function estimateTokens(text) {
8
+ if (!text) return 0;
9
+ if (typeof text !== 'string') {
10
+ text = JSON.stringify(text);
11
+ }
12
+ return Math.ceil(text.length / 4);
13
+ }
14
+
15
+ /**
16
+ * Count tokens in a full API payload
17
+ */
18
+ function countPayloadTokens(payload) {
19
+ const breakdown = {
20
+ system: 0,
21
+ tools: 0,
22
+ messages: 0,
23
+ total: 0
24
+ };
25
+
26
+ // System prompt
27
+ if (payload.system) {
28
+ if (Array.isArray(payload.system)) {
29
+ breakdown.system = payload.system.reduce((sum, block) =>
30
+ sum + estimateTokens(block.text || block), 0);
31
+ } else {
32
+ breakdown.system = estimateTokens(payload.system);
33
+ }
34
+ }
35
+
36
+ // Tools
37
+ if (payload.tools && Array.isArray(payload.tools)) {
38
+ breakdown.tools = estimateTokens(JSON.stringify(payload.tools));
39
+ }
40
+
41
+ // Messages
42
+ if (payload.messages && Array.isArray(payload.messages)) {
43
+ for (const msg of payload.messages) {
44
+ // Message content
45
+ if (typeof msg.content === 'string') {
46
+ breakdown.messages += estimateTokens(msg.content);
47
+ } else if (Array.isArray(msg.content)) {
48
+ breakdown.messages += msg.content.reduce((sum, block) => {
49
+ if (block.type === 'text') {
50
+ return sum + estimateTokens(block.text || '');
51
+ } else if (block.type === 'tool_result') {
52
+ return sum + estimateTokens(block.content || '');
53
+ } else if (block.type === 'image') {
54
+ // Images: rough estimate based on source length
55
+ return sum + estimateTokens(JSON.stringify(block.source || {}));
56
+ }
57
+ return sum + estimateTokens(JSON.stringify(block));
58
+ }, 0);
59
+ }
60
+
61
+ // Tool calls
62
+ if (msg.tool_calls) {
63
+ breakdown.messages += estimateTokens(JSON.stringify(msg.tool_calls));
64
+ }
65
+ }
66
+ }
67
+
68
+ breakdown.total = breakdown.system + breakdown.tools + breakdown.messages;
69
+ return breakdown;
70
+ }
71
+
72
+ /**
73
+ * Extract token usage from API response
74
+ */
75
+ function extractUsageFromResponse(response) {
76
+ if (!response || !response.usage) {
77
+ return null;
78
+ }
79
+
80
+ return {
81
+ inputTokens: response.usage.input_tokens || 0,
82
+ outputTokens: response.usage.output_tokens || 0,
83
+ cacheCreationTokens: response.usage.cache_creation_input_tokens || 0,
84
+ cacheReadTokens: response.usage.cache_read_input_tokens || 0,
85
+ totalTokens: (response.usage.input_tokens || 0) + (response.usage.output_tokens || 0)
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Calculate cost based on token usage
91
+ * Prices as of 2025 (update as needed)
92
+ */
93
+ function calculateCost(usage, model = 'claude-sonnet-4-5') {
94
+ const PRICES = {
95
+ 'claude-opus-4-5': { input: 15, output: 75, cache_write: 18.75, cache_read: 1.5 },
96
+ 'claude-sonnet-4-5': { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
97
+ 'claude-haiku-4': { input: 0.8, output: 4, cache_write: 1, cache_read: 0.08 },
98
+ 'databricks-claude-sonnet-4-5': { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
99
+ 'databricks-claude-haiku-4': { input: 0.8, output: 4, cache_write: 1, cache_read: 0.08 },
100
+ };
101
+
102
+ const price = PRICES[model] || PRICES['claude-sonnet-4-5'];
103
+
104
+ const inputCost = (usage.inputTokens / 1_000_000) * price.input;
105
+ const outputCost = (usage.outputTokens / 1_000_000) * price.output;
106
+ const cacheWriteCost = ((usage.cacheCreationTokens || 0) / 1_000_000) * price.cache_write;
107
+ const cacheReadCost = ((usage.cacheReadTokens || 0) / 1_000_000) * price.cache_read;
108
+
109
+ return {
110
+ input: inputCost,
111
+ output: outputCost,
112
+ cacheWrite: cacheWriteCost,
113
+ cacheRead: cacheReadCost,
114
+ total: inputCost + outputCost + cacheWriteCost + cacheReadCost
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Log token usage with breakdown
120
+ */
121
+ function logTokenUsage(context, estimated, actual) {
122
+ const efficiency = actual ? ((actual.totalTokens / estimated.total) * 100).toFixed(1) : 'N/A';
123
+
124
+ logger.info({
125
+ context,
126
+ estimated: {
127
+ system: estimated.system,
128
+ tools: estimated.tools,
129
+ messages: estimated.messages,
130
+ total: estimated.total
131
+ },
132
+ actual: actual || 'not available',
133
+ estimateAccuracy: efficiency + '%'
134
+ }, 'Token usage tracked');
135
+ }
136
+
137
+ /**
138
+ * Store token usage in session metadata
139
+ */
140
+ function recordTokenUsage(session, turnId, estimated, actual, model) {
141
+ if (!session || !actual) return;
142
+
143
+ session.metadata = session.metadata || {};
144
+ session.metadata.tokenUsage = session.metadata.tokenUsage || [];
145
+
146
+ const cost = calculateCost(actual, model);
147
+
148
+ session.metadata.tokenUsage.push({
149
+ turn: turnId,
150
+ timestamp: Date.now(),
151
+ estimated,
152
+ actual,
153
+ cost,
154
+ model
155
+ });
156
+
157
+ // Track cumulative totals
158
+ session.metadata.totalTokens = (session.metadata.totalTokens || 0) + actual.totalTokens;
159
+ session.metadata.totalCost = (session.metadata.totalCost || 0) + cost.total;
160
+ }
161
+
162
+ /**
163
+ * Get token statistics for a session
164
+ */
165
+ function getSessionTokenStats(session) {
166
+ if (!session || !session.metadata || !session.metadata.tokenUsage) {
167
+ return {
168
+ turns: 0,
169
+ totalTokens: 0,
170
+ totalCost: 0,
171
+ averageTokensPerTurn: 0,
172
+ breakdown: []
173
+ };
174
+ }
175
+
176
+ const usage = session.metadata.tokenUsage;
177
+ const totalTokens = session.metadata.totalTokens || 0;
178
+ const totalCost = session.metadata.totalCost || 0;
179
+
180
+ return {
181
+ turns: usage.length,
182
+ totalTokens,
183
+ totalCost,
184
+ averageTokensPerTurn: usage.length > 0 ? Math.round(totalTokens / usage.length) : 0,
185
+ cacheHitRate: calculateCacheHitRate(usage),
186
+ breakdown: usage
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Calculate cache hit rate from usage history
192
+ */
193
+ function calculateCacheHitRate(usageHistory) {
194
+ if (!usageHistory || usageHistory.length === 0) return 0;
195
+
196
+ const totalCacheableTokens = usageHistory.reduce((sum, turn) => {
197
+ return sum + (turn.actual.inputTokens || 0);
198
+ }, 0);
199
+
200
+ const cachedTokens = usageHistory.reduce((sum, turn) => {
201
+ return sum + (turn.actual.cacheReadTokens || 0);
202
+ }, 0);
203
+
204
+ return totalCacheableTokens > 0
205
+ ? ((cachedTokens / totalCacheableTokens) * 100).toFixed(1)
206
+ : 0;
207
+ }
208
+
209
+ module.exports = {
210
+ estimateTokens,
211
+ countPayloadTokens,
212
+ extractUsageFromResponse,
213
+ calculateCost,
214
+ logTokenUsage,
215
+ recordTokenUsage,
216
+ getSessionTokenStats
217
+ };
@@ -682,5 +682,203 @@ describe("llama.cpp Integration", () => {
682
682
  assert.strictEqual(result.usage.input_tokens, 0);
683
683
  assert.strictEqual(result.usage.output_tokens, 0);
684
684
  });
685
+
686
+ it("should filter duplicate tool call JSON from content when tool_calls are present", () => {
687
+ process.env.MODEL_PROVIDER = "databricks";
688
+ process.env.DATABRICKS_API_KEY = "test-key";
689
+ process.env.DATABRICKS_API_BASE = "http://test.com";
690
+
691
+ const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
692
+
693
+ // Simulate llama.cpp response with BOTH content (as JSON) and tool_calls
694
+ const response = {
695
+ id: "chatcmpl-123",
696
+ choices: [
697
+ {
698
+ message: {
699
+ role: "assistant",
700
+ content: '{"type": "function", "function": {"name": "Write", "parameters": {"file_path": "test.cpp", "content": "int main() {}"}}}',
701
+ tool_calls: [
702
+ {
703
+ id: "call_abc123",
704
+ type: "function",
705
+ function: {
706
+ name: "Write",
707
+ arguments: '{"file_path": "test.cpp", "content": "int main() {}"}'
708
+ }
709
+ }
710
+ ]
711
+ },
712
+ finish_reason: "tool_calls"
713
+ }
714
+ ],
715
+ usage: {
716
+ prompt_tokens: 100,
717
+ completion_tokens: 50
718
+ }
719
+ };
720
+
721
+ const result = convertOpenRouterResponseToAnthropic(response, "test-model");
722
+
723
+ // Should have only 1 content block (tool_use), not 2 (text + tool_use)
724
+ assert.strictEqual(result.content.length, 1);
725
+ assert.strictEqual(result.content[0].type, "tool_use");
726
+ assert.strictEqual(result.content[0].name, "Write");
727
+ assert.strictEqual(result.stop_reason, "tool_use");
728
+
729
+ // Verify the JSON text was NOT included as a text block
730
+ const textBlocks = result.content.filter(block => block.type === "text");
731
+ assert.strictEqual(textBlocks.length, 0, "Should not include text block with duplicate JSON");
732
+ });
733
+
734
+ it("should preserve normal text content when tool_calls are NOT present", () => {
735
+ process.env.MODEL_PROVIDER = "databricks";
736
+ process.env.DATABRICKS_API_KEY = "test-key";
737
+ process.env.DATABRICKS_API_BASE = "http://test.com";
738
+
739
+ const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
740
+
741
+ const response = {
742
+ id: "chatcmpl-456",
743
+ choices: [
744
+ {
745
+ message: {
746
+ role: "assistant",
747
+ content: "Here is the code you requested.",
748
+ // No tool_calls
749
+ },
750
+ finish_reason: "stop"
751
+ }
752
+ ],
753
+ usage: {
754
+ prompt_tokens: 50,
755
+ completion_tokens: 25
756
+ }
757
+ };
758
+
759
+ const result = convertOpenRouterResponseToAnthropic(response, "test-model");
760
+
761
+ // Should have 1 text block
762
+ assert.strictEqual(result.content.length, 1);
763
+ assert.strictEqual(result.content[0].type, "text");
764
+ assert.strictEqual(result.content[0].text, "Here is the code you requested.");
765
+ assert.strictEqual(result.stop_reason, "end_turn");
766
+ });
767
+
768
+ it("should preserve text content with tool_calls when text is NOT JSON", () => {
769
+ process.env.MODEL_PROVIDER = "databricks";
770
+ process.env.DATABRICKS_API_KEY = "test-key";
771
+ process.env.DATABRICKS_API_BASE = "http://test.com";
772
+
773
+ const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
774
+
775
+ // Some models include explanatory text before/with tool calls
776
+ const response = {
777
+ id: "chatcmpl-789",
778
+ choices: [
779
+ {
780
+ message: {
781
+ role: "assistant",
782
+ content: "I'll write the file for you now.",
783
+ tool_calls: [
784
+ {
785
+ id: "call_xyz789",
786
+ type: "function",
787
+ function: {
788
+ name: "Write",
789
+ arguments: '{"file_path": "test.cpp", "content": "int main() {}"}'
790
+ }
791
+ }
792
+ ]
793
+ },
794
+ finish_reason: "tool_calls"
795
+ }
796
+ ],
797
+ usage: {
798
+ prompt_tokens: 100,
799
+ completion_tokens: 60
800
+ }
801
+ };
802
+
803
+ const result = convertOpenRouterResponseToAnthropic(response, "test-model");
804
+
805
+ // Should have 2 content blocks (text + tool_use)
806
+ assert.strictEqual(result.content.length, 2);
807
+ assert.strictEqual(result.content[0].type, "text");
808
+ assert.strictEqual(result.content[0].text, "I'll write the file for you now.");
809
+ assert.strictEqual(result.content[1].type, "tool_use");
810
+ assert.strictEqual(result.content[1].name, "Write");
811
+ assert.strictEqual(result.stop_reason, "tool_use");
812
+ });
813
+
814
+ it("should filter malformed JSON when model outputs ONLY JSON without tool_calls", () => {
815
+ process.env.MODEL_PROVIDER = "databricks";
816
+ process.env.DATABRICKS_API_KEY = "test-key";
817
+ process.env.DATABRICKS_API_BASE = "http://test.com";
818
+
819
+ const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
820
+
821
+ // Simulate llama.cpp model that outputs JSON in content but doesn't provide tool_calls
822
+ // This is a model training/configuration issue - model learned to output JSON
823
+ // but llama.cpp server isn't converting it to structured tool_calls
824
+ const response = {
825
+ id: "chatcmpl-malformed",
826
+ choices: [
827
+ {
828
+ message: {
829
+ role: "assistant",
830
+ content: '{"function": "Write", "parameters": {"file_path": "test.go", "content": "package main"}}',
831
+ // No tool_calls array - model error!
832
+ },
833
+ finish_reason: "stop"
834
+ }
835
+ ],
836
+ usage: {
837
+ prompt_tokens: 50,
838
+ completion_tokens: 30
839
+ }
840
+ };
841
+
842
+ const result = convertOpenRouterResponseToAnthropic(response, "test-model");
843
+
844
+ // Should have 1 empty text block (JSON was filtered out)
845
+ assert.strictEqual(result.content.length, 1);
846
+ assert.strictEqual(result.content[0].type, "text");
847
+ assert.strictEqual(result.content[0].text, "");
848
+ assert.strictEqual(result.stop_reason, "end_turn");
849
+ });
850
+
851
+ it("should filter alternative JSON formats without tool_calls", () => {
852
+ process.env.MODEL_PROVIDER = "databricks";
853
+ process.env.DATABRICKS_API_KEY = "test-key";
854
+ process.env.DATABRICKS_API_BASE = "http://test.com";
855
+
856
+ const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
857
+
858
+ // Test the other JSON format seen in the wild
859
+ const response = {
860
+ id: "chatcmpl-alt-format",
861
+ choices: [
862
+ {
863
+ message: {
864
+ role: "assistant",
865
+ content: '{"type": "function", "function": {"name": "Read", "arguments": {"file_path": "config.json"}}}',
866
+ },
867
+ finish_reason: "stop"
868
+ }
869
+ ],
870
+ usage: {
871
+ prompt_tokens: 40,
872
+ completion_tokens: 25
873
+ }
874
+ };
875
+
876
+ const result = convertOpenRouterResponseToAnthropic(response, "test-model");
877
+
878
+ // Should filter out the JSON
879
+ assert.strictEqual(result.content.length, 1);
880
+ assert.strictEqual(result.content[0].type, "text");
881
+ assert.strictEqual(result.content[0].text, "");
882
+ });
685
883
  });
686
884
  });
@@ -12,11 +12,13 @@ describe("Memory Extractor", () => {
12
12
  // Save original environment
13
13
  originalEnv = { ...process.env };
14
14
 
15
- // Create a temporary test database
16
- testDbPath = path.join(__dirname, `../../data/test-extractor-${Date.now()}.db`);
15
+ // Create a unique temporary test database
16
+ const timestamp = Date.now();
17
+ const random = Math.floor(Math.random() * 1000000);
18
+ testDbPath = path.join(__dirname, `../../data/test-extractor-${timestamp}-${random}.db`);
17
19
 
18
20
  // Set test environment BEFORE loading any modules
19
- process.env.DB_PATH = testDbPath;
21
+ process.env.SESSION_DB_PATH = testDbPath;
20
22
  process.env.MEMORY_SURPRISE_THRESHOLD = "0.1"; // Very low threshold for tests
21
23
  process.env.MEMORY_ENABLED = "true";
22
24
  process.env.MEMORY_EXTRACTION_ENABLED = "true";
@@ -36,13 +38,39 @@ describe("Memory Extractor", () => {
36
38
  });
37
39
 
38
40
  afterEach(() => {
41
+ // Close database connection first
42
+ try {
43
+ const db = require("../../src/db");
44
+ if (db && typeof db.close === 'function') {
45
+ db.close();
46
+ }
47
+ } catch (err) {
48
+ // Ignore if already closed
49
+ }
50
+
51
+ // Clear module cache to release all references
52
+ Object.keys(require.cache).forEach(key => {
53
+ if (key.includes('/src/')) {
54
+ delete require.cache[key];
55
+ }
56
+ });
57
+
39
58
  // Restore environment
40
59
  process.env = originalEnv;
41
60
 
42
- // Clean up test database
61
+ // Clean up all SQLite files (db, wal, shm)
43
62
  try {
44
- if (fs.existsSync(testDbPath)) {
45
- fs.unlinkSync(testDbPath);
63
+ const files = [
64
+ testDbPath,
65
+ `${testDbPath}-wal`,
66
+ `${testDbPath}-shm`,
67
+ `${testDbPath}-journal`
68
+ ];
69
+
70
+ for (const file of files) {
71
+ if (fs.existsSync(file)) {
72
+ fs.unlinkSync(file);
73
+ }
46
74
  }
47
75
  } catch (err) {
48
76
  // Ignore cleanup errors
@@ -9,19 +9,22 @@ describe("Memory Retriever", () => {
9
9
  let testDbPath;
10
10
 
11
11
  beforeEach(() => {
12
- // Create a temporary test database
13
- testDbPath = path.join(__dirname, `../../data/test-memory-${Date.now()}.db`);
12
+ // Create a unique temporary test database
13
+ const timestamp = Date.now();
14
+ const random = Math.floor(Math.random() * 1000000);
15
+ testDbPath = path.join(__dirname, `../../data/test-retriever-${timestamp}-${random}.db`);
14
16
 
15
- // Clear module cache
17
+ // Set test environment to new database (correct env var is SESSION_DB_PATH)
18
+ process.env.SESSION_DB_PATH = testDbPath;
19
+
20
+ // Clear ALL module cache to ensure fresh config is loaded
21
+ delete require.cache[require.resolve("../../src/config")];
16
22
  delete require.cache[require.resolve("../../src/db")];
17
23
  delete require.cache[require.resolve("../../src/memory/store")];
18
24
  delete require.cache[require.resolve("../../src/memory/search")];
19
25
  delete require.cache[require.resolve("../../src/memory/retriever")];
20
26
 
21
- // Set test environment
22
- process.env.DB_PATH = testDbPath;
23
-
24
- // Initialize database with schema
27
+ // Initialize database with schema (this creates a fresh database)
25
28
  require("../../src/db");
26
29
 
27
30
  // Load modules
@@ -84,10 +87,35 @@ describe("Memory Retriever", () => {
84
87
  });
85
88
 
86
89
  afterEach(() => {
87
- // Clean up test database
90
+ // Close database connection first
88
91
  try {
89
- if (fs.existsSync(testDbPath)) {
90
- fs.unlinkSync(testDbPath);
92
+ const db = require("../../src/db");
93
+ if (db && typeof db.close === 'function') {
94
+ db.close();
95
+ }
96
+ } catch (err) {
97
+ // Ignore if already closed
98
+ }
99
+
100
+ // Clear module cache to release all references
101
+ delete require.cache[require.resolve("../../src/db")];
102
+ delete require.cache[require.resolve("../../src/memory/store")];
103
+ delete require.cache[require.resolve("../../src/memory/search")];
104
+ delete require.cache[require.resolve("../../src/memory/retriever")];
105
+
106
+ // Clean up all SQLite files (db, wal, shm)
107
+ try {
108
+ const files = [
109
+ testDbPath,
110
+ `${testDbPath}-wal`,
111
+ `${testDbPath}-shm`,
112
+ `${testDbPath}-journal`
113
+ ];
114
+
115
+ for (const file of files) {
116
+ if (fs.existsSync(file)) {
117
+ fs.unlinkSync(file);
118
+ }
91
119
  }
92
120
  } catch (err) {
93
121
  // Ignore cleanup errors
@@ -412,9 +440,11 @@ describe("Memory Retriever", () => {
412
440
  );
413
441
 
414
442
  assert.ok(typeof systemFormat === "string");
415
- assert.ok(typeof preambleFormat === "string");
416
- // Both should include memory content
417
- assert.ok(systemFormat.length > 0 || preambleFormat.length > 0);
443
+ // assistant_preamble format returns an object
444
+ assert.ok(typeof preambleFormat === "object");
445
+ assert.ok(preambleFormat.system === "You are helpful.");
446
+ assert.ok(typeof preambleFormat.memoryPreamble === "string");
447
+ assert.ok(preambleFormat.memoryPreamble.length > 0);
418
448
  });
419
449
  });
420
450
 
@@ -455,11 +485,11 @@ describe("Memory Retriever", () => {
455
485
  store.createMemory({
456
486
  content: "Session memory",
457
487
  type: "fact",
458
- sessionId: "test-session"
488
+ sessionId: null // was: "test-session"
459
489
  });
460
490
 
461
491
  const globalStats = retriever.getMemoryStats();
462
- const sessionStats = retriever.getMemoryStats({ sessionId: "test-session" });
492
+ const sessionStats = retriever.getMemoryStats({ sessionId: null }); // was: "test-session"
463
493
 
464
494
  assert.ok(sessionStats.total <= globalStats.total);
465
495
  });