@aj-archipelago/cortex 1.3.21 → 1.3.22

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 (32) hide show
  1. package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +2 -2
  2. package/lib/util.js +1 -1
  3. package/package.json +1 -1
  4. package/pathways/system/entity/memory/shared/sys_memory_helpers.js +228 -0
  5. package/pathways/system/entity/memory/sys_memory_format.js +30 -0
  6. package/pathways/system/entity/memory/sys_memory_manager.js +85 -27
  7. package/pathways/system/entity/memory/sys_memory_process.js +154 -0
  8. package/pathways/system/entity/memory/sys_memory_required.js +4 -2
  9. package/pathways/system/entity/memory/sys_memory_topic.js +22 -0
  10. package/pathways/system/entity/memory/sys_memory_update.js +50 -150
  11. package/pathways/system/entity/memory/sys_read_memory.js +67 -69
  12. package/pathways/system/entity/memory/sys_save_memory.js +1 -1
  13. package/pathways/system/entity/memory/sys_search_memory.js +1 -1
  14. package/pathways/system/entity/sys_entity_start.js +9 -6
  15. package/pathways/system/entity/sys_generator_image.js +5 -41
  16. package/pathways/system/entity/sys_generator_memory.js +3 -1
  17. package/pathways/system/entity/sys_generator_reasoning.js +1 -1
  18. package/pathways/system/entity/sys_router_tool.js +3 -4
  19. package/pathways/system/rest_streaming/sys_claude_35_sonnet.js +1 -1
  20. package/pathways/system/rest_streaming/sys_claude_3_haiku.js +1 -1
  21. package/pathways/system/rest_streaming/sys_google_gemini_chat.js +1 -1
  22. package/pathways/system/rest_streaming/sys_openai_chat_o1.js +1 -1
  23. package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +1 -1
  24. package/pathways/transcribe_gemini.js +397 -0
  25. package/server/pathwayResolver.js +7 -7
  26. package/server/plugins/claude3VertexPlugin.js +109 -3
  27. package/server/plugins/gemini15VisionPlugin.js +7 -0
  28. package/server/plugins/modelPlugin.js +1 -1
  29. package/server/rest.js +24 -3
  30. package/tests/claude3VertexToolConversion.test.js +411 -0
  31. package/tests/memoryfunction.test.js +560 -46
  32. package/tests/openai_api.test.js +332 -0
@@ -1,23 +1,24 @@
1
1
  import test from 'ava';
2
- import { enforceTokenLimit, modifyText } from '../pathways/system/entity/memory/sys_memory_update.js';
2
+ import { modifyText, enforceTokenLimit } from '../pathways/system/entity/memory/shared/sys_memory_helpers.js';
3
+ import { processMemoryContent } from '../pathways/system/entity/memory/sys_read_memory.js';
3
4
 
4
5
  test('enforceTokenLimit preserves priority order correctly', t => {
5
6
  const input = `
6
- [P1] Highest priority item
7
- [P2] High priority item
8
- [P3] Medium priority item
9
- [P4] Low priority item
10
- [P5] Lowest priority item`.trim();
7
+ 1|2024-03-19T10:00:00Z|Highest priority item
8
+ 2|2024-03-19T11:00:00Z|High priority item
9
+ 3|2024-03-19T12:00:00Z|Medium priority item
10
+ 4|2024-03-19T13:00:00Z|Low priority item
11
+ 5|2024-03-19T14:00:00Z|Lowest priority item`.trim();
11
12
 
12
13
  // Enforce trimming
13
- const result = enforceTokenLimit(input, 20);
14
+ const result = enforceTokenLimit(input, 40);
14
15
 
15
16
  // Should keep P1, P2 only
16
- t.true(result.includes('[P1]'));
17
- t.true(result.includes('[P2]'));
18
- t.false(result.includes('[P3]'));
19
- t.false(result.includes('[P4]'));
20
- t.false(result.includes('[P5]'));
17
+ t.true(result.includes('1|2024-03-19T10:00:00Z|Highest priority item'));
18
+ t.true(result.includes('2|2024-03-19T11:00:00Z|High priority item'));
19
+ t.false(result.includes('3|2024-03-19T12:00:00Z|Medium priority item'));
20
+ t.false(result.includes('4|2024-03-19T13:00:00Z|Low priority item'));
21
+ t.false(result.includes('5|2024-03-19T14:00:00Z|Lowest priority item'));
21
22
  });
22
23
 
23
24
  test('enforceTokenLimit handles empty input', t => {
@@ -27,9 +28,9 @@ test('enforceTokenLimit handles empty input', t => {
27
28
 
28
29
  test('enforceTokenLimit removes duplicates', t => {
29
30
  const input = `
30
- [P3] Duplicate item
31
- [P3] Duplicate item
32
- [P2] Unique item`.trim();
31
+ 3|2024-03-19T10:00:00Z|Duplicate item
32
+ 3|2024-03-19T11:00:00Z|Duplicate item
33
+ 2|2024-03-19T12:00:00Z|Unique item`.trim();
33
34
 
34
35
  const result = enforceTokenLimit(input);
35
36
 
@@ -40,9 +41,9 @@ test('enforceTokenLimit removes duplicates', t => {
40
41
 
41
42
  test('enforceTokenLimit handles topics section differently', t => {
42
43
  const input = `
43
- 2024-03-19T10:00:00Z Discussed AI ethics
44
- 2024-03-19T11:00:00Z Discussed programming
45
- 2024-03-19T12:00:00Z Discussed testing`.trim();
44
+ 3|2024-03-19T10:00:00Z|Discussed AI ethics
45
+ 2|2024-03-19T11:00:00Z|Discussed programming
46
+ 1|2024-03-19T12:00:00Z|Discussed testing`.trim();
46
47
 
47
48
  const result = enforceTokenLimit(input, 20, true);
48
49
 
@@ -52,22 +53,11 @@ test('enforceTokenLimit handles topics section differently', t => {
52
53
  t.true(result.includes('12:00:00Z'));
53
54
  });
54
55
 
55
- test('enforceTokenLimit adds P3 to unprioritized lines', t => {
56
- const input = `
57
- Item without priority
58
- [P1] Item with priority`.trim();
59
-
60
- const result = enforceTokenLimit(input);
61
-
62
- t.true(result.includes('[P3] Item without priority'));
63
- t.true(result.includes('[P1] Item with priority'));
64
- });
65
-
66
56
  test('modifyText handles delete operations with escaped characters', t => {
67
- const input = '[P2] Pizza Connection: Has special appreciation for Pisanello\'s';
57
+ const input = '2|2024-03-19T10:00:00Z|Pizza Connection: Has special appreciation for Pisanello\'s';
68
58
  const modifications = [{
69
59
  type: 'delete',
70
- pattern: '\\[P2\\] Pizza Connection: Has special appreciation for Pisanello\'s'
60
+ pattern: 'Pizza Connection: Has special appreciation for Pisanello\'s'
71
61
  }];
72
62
 
73
63
  const result = modifyText(input, modifications);
@@ -75,7 +65,7 @@ test('modifyText handles delete operations with escaped characters', t => {
75
65
  });
76
66
 
77
67
  test('modifyText handles delete with partial priority match', t => {
78
- const input = '[P3] Test memory item';
68
+ const input = '3|2024-03-19T10:00:00Z|Test memory item';
79
69
  const modifications = [{
80
70
  type: 'delete',
81
71
  pattern: 'Test memory item'
@@ -86,7 +76,7 @@ test('modifyText handles delete with partial priority match', t => {
86
76
  });
87
77
 
88
78
  test('modifyText handles multiple modifications in sequence', t => {
89
- const input = '[P2] Keep this line\n[P3] Delete this line\n[P1] Also keep this';
79
+ const input = '2|2024-03-19T10:00:00Z|Keep this line\n3|2024-03-19T11:00:00Z|Delete this line\n1|2024-03-19T12:00:00Z|Also keep this';
90
80
  const modifications = [
91
81
  { type: 'delete', pattern: 'Delete this line' },
92
82
  { type: 'add', newtext: 'New line added', priority: '2' }
@@ -95,12 +85,12 @@ test('modifyText handles multiple modifications in sequence', t => {
95
85
  const result = modifyText(input, modifications);
96
86
  t.true(result.includes('Keep this line'));
97
87
  t.true(result.includes('Also keep this'));
98
- t.true(result.includes('[P2] New line added'));
88
+ t.true(result.includes('New line added'));
99
89
  t.false(result.includes('Delete this line'));
100
90
  });
101
91
 
102
92
  test('modifyText handles delete with whitespace variations', t => {
103
- const input = ' [P2] Item with spaces \n[P3]Item without spaces';
93
+ const input = ' 2|2024-03-19T10:00:00Z|Item with spaces \n3|2024-03-19T11:00:00Z|Item without spaces';
104
94
  const modifications = [
105
95
  { type: 'delete', pattern: 'Item with spaces' },
106
96
  { type: 'delete', pattern: 'Item without spaces' }
@@ -111,26 +101,550 @@ test('modifyText handles delete with whitespace variations', t => {
111
101
  t.false(result.includes('Item without spaces'));
112
102
  });
113
103
 
114
- test('modifyText preserves existing priority when adding with priority in text', t => {
115
- const input = '';
104
+ test('modifyText handles delete with regex special characters', t => {
105
+ const input = '2|2024-03-19T10:00:00Z|Special (chars) [test] {here} *star*';
116
106
  const modifications = [{
117
- type: 'add',
118
- newtext: '[P1] High priority item',
119
- priority: '3' // This should be ignored since priority is in text
107
+ type: 'delete',
108
+ pattern: 'Special \\(chars\\) \\[test\\] \\{here\\} \\*star\\*'
120
109
  }];
121
110
 
122
111
  const result = modifyText(input, modifications);
123
- t.true(result.includes('[P1] High priority item'));
124
- t.false(result.includes('[P3] [P1] High priority item'));
112
+ t.false(result.includes('Special (chars)'));
125
113
  });
126
114
 
127
- test('modifyText handles delete with regex special characters', t => {
128
- const input = '[P2] Special (chars) [test] {here} *star*';
115
+ test('modifyText handles content with pipe characters', t => {
116
+ const input = '2|2024-03-19T10:00:00Z|Memory about|pipes|in|content';
117
+ const modifications = [{
118
+ type: 'change',
119
+ pattern: 'Memory about\\|pipes\\|in\\|content',
120
+ newtext: 'Updated memory'
121
+ }];
122
+
123
+ const result = modifyText(input, modifications);
124
+ t.true(result.includes('Updated memory'));
125
+ t.false(result.includes('Memory about|pipes|in|content'));
126
+ });
127
+
128
+ test('modifyText performs case insensitive matching', t => {
129
+ const input = '2|2024-03-19T10:00:00Z|UPPER CASE Memory\n3|2024-03-19T11:00:00Z|lower case memory';
129
130
  const modifications = [{
130
131
  type: 'delete',
131
- pattern: 'Special \\(chars\\) \\[test\\] \\{here\\} \\*star\\*'
132
+ pattern: 'upper case memory'
132
133
  }];
133
134
 
134
135
  const result = modifyText(input, modifications);
135
- t.false(result.includes('Special (chars)'));
136
+ t.false(result.includes('UPPER CASE Memory'));
137
+ });
138
+
139
+ test('modifyText handles multiple overlapping patterns', t => {
140
+ const input = `
141
+ 2|2024-03-19T10:00:00Z|Memory about AI and ML
142
+ 3|2024-03-19T11:00:00Z|Memory about AI and robotics
143
+ 1|2024-03-19T12:00:00Z|Memory about ML and data`.trim();
144
+
145
+ const modifications = [
146
+ { type: 'delete', pattern: '.*AI.*' },
147
+ { type: 'change', pattern: '.*ML.*', newtext: 'Updated ML memory' }
148
+ ];
149
+
150
+ const result = modifyText(input, modifications);
151
+ t.false(result.includes('Memory about AI'));
152
+ t.true(result.includes('Updated ML memory'));
153
+ t.is((result.match(/Updated ML memory/g) || []).length, 1);
154
+ });
155
+
156
+ test('modifyText preserves and updates priorities correctly', t => {
157
+ const input = '2|2024-03-19T10:00:00Z|Original priority 2 memory';
158
+
159
+ const modifications = [{
160
+ type: 'change',
161
+ pattern: 'Original priority 2 memory',
162
+ newtext: 'Changed memory',
163
+ priority: '1'
164
+ }];
165
+
166
+ const result = modifyText(input, modifications);
167
+ // Check priority was updated to 1
168
+ t.true(result.startsWith('1|'));
169
+ // Check content was updated
170
+ t.true(result.endsWith('|Changed memory'));
171
+ // Verify timestamp format
172
+ t.regex(result, /^1\|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z\|Changed memory$/);
173
+ });
174
+
175
+ test('modifyText handles empty and invalid modifications', t => {
176
+ const input = '2|2024-03-19T10:00:00Z|Test memory';
177
+
178
+ const modifications = [
179
+ { type: 'delete' }, // Missing pattern
180
+ { type: 'change', pattern: 'Test' }, // Missing newtext
181
+ { type: 'invalid', pattern: 'Test', newtext: 'Invalid' }, // Invalid type
182
+ { type: 'add' } // Missing newtext
183
+ ];
184
+
185
+ const result = modifyText(input, modifications);
186
+ // Check that priority and content remain unchanged
187
+ t.true(result.startsWith('2|'));
188
+ t.true(result.includes('|Test memory'));
189
+ // Verify timestamp format
190
+ t.regex(result, /^2\|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z\|Test memory$/);
191
+ });
192
+
193
+ test('modifyText handles regex patterns in change operations', t => {
194
+ const input = '2|2024-03-19T10:00:00Z|Memory from 2024-03-19';
195
+
196
+ const modifications = [{
197
+ type: 'change',
198
+ pattern: 'Memory from \\d{4}-\\d{2}-\\d{2}',
199
+ newtext: 'Updated date memory'
200
+ }];
201
+
202
+ const result = modifyText(input, modifications);
203
+ t.true(result.includes('Updated date memory'));
204
+ t.false(result.includes('Memory from 2024'));
205
+ });
206
+
207
+ test('modifyText handles malformed memory lines gracefully', t => {
208
+ const input = `
209
+ no_separators_at_all
210
+ 2|missing_content
211
+ |2024-03-19T10:00:00Z|missing_priority
212
+ 2||missing_timestamp|content
213
+ 2|invalid|timestamp|here|content
214
+ |||| empty parts
215
+ `.trim();
216
+
217
+ const modifications = [{
218
+ type: 'add',
219
+ newtext: 'New valid memory'
220
+ }];
221
+
222
+ const result = modifyText(input, modifications);
223
+ // Should preserve valid parts and add new memory
224
+ t.true(result.includes('New valid memory'));
225
+ // Should handle malformed lines without crashing
226
+ t.true(result.split('\n').length > 0);
227
+ // Verify output format for the added line
228
+ t.regex(result, /3\|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z\|New valid memory$/m);
229
+ });
230
+
231
+ test('modifyText handles memory with special regex characters', t => {
232
+ const input = '2|2024-03-19T10:00:00Z|Memory with (parens) and [brackets] and {braces} and +*?^$\\.';
233
+
234
+ // Try to modify it with a pattern containing regex special chars
235
+ const modifications = [{
236
+ type: 'change',
237
+ pattern: 'Memory with \\(parens\\) and \\[brackets\\]',
238
+ newtext: 'Updated memory'
239
+ }];
240
+
241
+ const result = modifyText(input, modifications);
242
+ t.true(result.includes('Updated memory'));
243
+ t.false(result.includes('Memory with (parens)'));
244
+ });
245
+
246
+ test('modifyText handles extremely long memory lines', t => {
247
+ const longContent = 'a'.repeat(1000);
248
+ const input = `2|2024-03-19T10:00:00Z|${longContent}`;
249
+
250
+ const modifications = [{
251
+ type: 'change',
252
+ pattern: 'a+',
253
+ newtext: 'Shortened memory'
254
+ }];
255
+
256
+ const result = modifyText(input, modifications);
257
+ t.true(result.includes('Shortened memory'));
258
+ t.false(result.includes(longContent));
259
+ });
260
+
261
+ test('modifyText handles unicode and special characters', t => {
262
+ const input = '2|2024-03-19T10:00:00Z|Memory with 🚀 emoji and üñîçødé and "quotes" and \'apostrophes\'';
263
+
264
+ const modifications = [{
265
+ type: 'change',
266
+ pattern: 'Memory with.*emoji',
267
+ newtext: 'Updated with more 🎉 emoji'
268
+ }];
269
+
270
+ const result = modifyText(input, modifications);
271
+ t.true(result.includes('Updated with more 🎉 emoji'));
272
+ t.false(result.includes('Memory with 🚀 emoji'));
273
+ });
274
+
275
+ test('modifyText handles various regex pattern types', t => {
276
+ const input = `
277
+ 2|2024-03-19T10:00:00Z|Start of line memory
278
+ 3|2024-03-19T11:00:00Z|Memory in the middle
279
+ 1|2024-03-19T12:00:00Z|Memory at the end
280
+ 2|2024-03-19T13:00:00Z|12345 numeric memory
281
+ 3|2024-03-19T14:00:00Z|Multiple spaces here
282
+ 2|2024-03-19T15:00:00Z|Line.with.dots
283
+ 2|2024-03-19T16:00:00Z|Line_with_underscores
284
+ 2|2024-03-19T17:00:00Z|This is a long memory that we want to match partially`.trim();
285
+
286
+ const modifications = [
287
+ { type: 'delete', pattern: '^Start.*memory$' }, // Start of line to end
288
+ { type: 'delete', pattern: 'Memory at the end$' }, // End of line
289
+ { type: 'delete', pattern: '\\d{5}.*memory' }, // Numbers followed by text
290
+ { type: 'change', pattern: 'Multiple\\s+spaces\\s+here', newtext: 'Single Space', priority: '1' }, // Multiple spaces
291
+ { type: 'delete', pattern: 'Line\\.with\\.dots' }, // Escaped dots
292
+ { type: 'delete', pattern: 'Line_with_underscores' }, // Underscores
293
+ { type: 'change', pattern: '^This.*partially$', newtext: 'Shortened' } // Full line match
294
+ ];
295
+
296
+ const result = modifyText(input, modifications);
297
+ t.false(result.includes('Start of line memory'));
298
+ t.false(result.includes('Memory at the end'));
299
+ t.false(result.includes('12345'));
300
+ t.true(result.includes('Single Space'));
301
+ t.false(result.includes('Line.with.dots'));
302
+ t.false(result.includes('Line_with_underscores'));
303
+ t.true(result.includes('Shortened'));
304
+ });
305
+
306
+ test('modifyText handles regex pattern edge cases', t => {
307
+ const input = `
308
+ 2|2024-03-19T10:00:00Z|Empty string match:
309
+ 3|2024-03-19T11:00:00Z|Zero-width match: abc
310
+ 1|2024-03-19T12:00:00Z|Lookahead: testing123
311
+ 2|2024-03-19T13:00:00Z|Backreference: hello hello
312
+ 3|2024-03-19T14:00:00Z|Non-greedy: <tag>content</tag>`.trim();
313
+
314
+ const modifications = [
315
+ { type: 'change', pattern: '^Empty string match:\\s*$', newtext: 'EMPTY' }, // Empty string
316
+ { type: 'change', pattern: 'Zero-width match: \\w{3}', newtext: 'ZERO_WIDTH' }, // Word chars
317
+ { type: 'change', pattern: 'Lookahead: \\w+\\d+', newtext: 'LOOKAHEAD' }, // Word chars followed by numbers
318
+ { type: 'change', pattern: 'Backreference: (\\w+)\\s\\1', newtext: 'REPEATED' }, // Same word repeated
319
+ { type: 'change', pattern: 'Non-greedy: <[^>]+>[^<]+</[^>]+>', newtext: 'TAG' } // XML-like tag
320
+ ];
321
+
322
+ const result = modifyText(input, modifications);
323
+ t.true(result.includes('EMPTY'));
324
+ t.true(result.includes('ZERO_WIDTH'));
325
+ t.true(result.includes('LOOKAHEAD'));
326
+ t.true(result.includes('REPEATED'));
327
+ t.true(result.includes('TAG'));
328
+ });
329
+
330
+ test('modifyText handles pattern groups and alternation', t => {
331
+ const input = `
332
+ 2|2024-03-19T10:00:00Z|Group1: apple orange
333
+ 3|2024-03-19T11:00:00Z|Group2: banana grape
334
+ 1|2024-03-19T12:00:00Z|Either: cat
335
+ 2|2024-03-19T13:00:00Z|Either: dog
336
+ 3|2024-03-19T14:00:00Z|Mixed: apple dog banana`.trim();
337
+
338
+ const modifications = [
339
+ { type: 'change', pattern: 'Group\\d: (apple|banana) \\w+', newtext: 'Category: $1 fruit' }, // Capture and alternation
340
+ { type: 'change', pattern: 'Either: (cat|dog)', newtext: 'Either: pet' }, // Simple alternation
341
+ { type: 'change', pattern: 'Mixed: (\\w+) (\\w+) (\\w+)', newtext: 'Mixed: $1 and $2 with $3' } // Multiple captures
342
+ ];
343
+
344
+ const result = modifyText(input, modifications);
345
+ t.true(result.includes('Category: apple fruit'));
346
+ t.true(result.includes('Category: banana fruit'));
347
+ t.true(result.includes('Either: pet'));
348
+ t.true(result.includes('Mixed: apple and dog with banana'));
349
+ });
350
+
351
+ test('enforceTokenLimit sorts topics by timestamp before trimming', t => {
352
+ const input = `
353
+ 3|2024-03-19T15:00:00Z|Newest topic
354
+ 3|2024-03-19T10:00:00Z|Older topic
355
+ 3|2024-03-19T12:00:00Z|Middle topic
356
+ 3|2024-03-19T14:00:00Z|Recent topic
357
+ 3|2024-03-19T11:00:00Z|Old topic`.trim();
358
+
359
+ // Set a small token limit to force trimming
360
+ const result = enforceTokenLimit(input, 40, true);
361
+
362
+ // Should keep newest topics
363
+ t.true(result.includes('Newest topic'));
364
+ t.true(result.includes('Recent topic'));
365
+ // Should remove oldest topics
366
+ t.false(result.includes('Older topic'));
367
+ t.false(result.includes('Old topic'));
368
+
369
+ // Verify order
370
+ const lines = result.split('\n');
371
+ t.true(lines[0].includes('Newest topic')); // First line should be newest
372
+ });
373
+
374
+ test('enforceTokenLimit handles missing timestamps in topics', t => {
375
+ const input = `
376
+ 3||Current topic
377
+ 3|2024-03-19T15:00:00Z|Newer topic
378
+ 3|2024-03-19T10:00:00Z|Older topic`.trim();
379
+
380
+ const result = enforceTokenLimit(input, 40, true);
381
+
382
+ // Missing timestamp should be treated as oldest
383
+ t.false(result.includes('Current topic'));
384
+ t.true(result.includes('Newer topic'));
385
+ });
386
+
387
+ test('enforceTokenLimit handles large content with pipes', t => {
388
+ const input = `
389
+ 1|2024-03-19T15:00:00Z|Memory with|multiple|pipes|in|content
390
+ 2|2024-03-19T14:00:00Z|Another|piped|memory
391
+ 3|2024-03-19T13:00:00Z|Simple memory`.trim();
392
+
393
+ const result = enforceTokenLimit(input, 50);
394
+
395
+ // Should preserve pipes in content when under limit
396
+ t.true(result.includes('Memory with|multiple|pipes|in|content'));
397
+ });
398
+
399
+ test('enforceTokenLimit removes duplicates while preserving newest timestamp', t => {
400
+ const input = `
401
+ 3|2024-03-19T10:00:00Z|Duplicate content
402
+ 3|2024-03-19T15:00:00Z|Duplicate content
403
+ 3|2024-03-19T12:00:00Z|Duplicate content
404
+ 2|2024-03-19T14:00:00Z|Unique content`.trim();
405
+
406
+ const result = enforceTokenLimit(input, 1000, true);
407
+
408
+ // Should only have one instance of duplicate content
409
+ t.is((result.match(/Duplicate content/g) || []).length, 1);
410
+ // Should keep the newest timestamp version
411
+ t.true(result.includes('2024-03-19T15:00:00Z|Duplicate content'));
412
+ });
413
+
414
+ test('enforceTokenLimit handles recursive trimming when estimation is off', t => {
415
+ // Create content that will need multiple trim passes
416
+ const input = Array.from({ length: 20 }, (_, i) =>
417
+ `${i % 3 + 1}|2024-03-19T${String(i).padStart(2, '0')}:00:00Z|Memory ${i} ${'x'.repeat(50)}`
418
+ ).join('\n');
419
+
420
+ const result = enforceTokenLimit(input, 200);
421
+
422
+ // Should have significantly fewer lines than input
423
+ t.true(result.split('\n').length < input.split('\n').length);
424
+ // Should still maintain priority order
425
+ const priorities = result.split('\n').map(line => parseInt(line.split('|')[0]));
426
+ t.deepEqual(priorities, [...priorities].sort((a, b) => b - a));
427
+ });
428
+
429
+ test('enforceTokenLimit preserves empty lines in non-topics mode', t => {
430
+ const input = `
431
+ 1|2024-03-19T15:00:00Z|First memory
432
+
433
+ 2|2024-03-19T14:00:00Z|Second memory
434
+
435
+ 3|2024-03-19T13:00:00Z|Third memory`.trim();
436
+
437
+ const result = enforceTokenLimit(input, 1000);
438
+
439
+ // Should preserve content but remove empty lines
440
+ t.true(result.includes('First memory'));
441
+ t.true(result.includes('Second memory'));
442
+ t.true(result.includes('Third memory'));
443
+ // Should not have empty lines
444
+ t.false(result.includes('\n\n'));
445
+ });
446
+
447
+ test('enforceTokenLimit handles malformed input gracefully', t => {
448
+ const input = `
449
+ invalid_line_no_separators
450
+ 1|2024-03-19T15:00:00Z|Valid memory
451
+ ||
452
+ 2||Invalid but with separators
453
+ 3|invalid_timestamp|Still valid content`.trim();
454
+
455
+ const result = enforceTokenLimit(input, 100, true);
456
+
457
+ // Should keep valid content
458
+ t.true(result.includes('Valid memory'));
459
+ // Should handle malformed lines without crashing
460
+ t.true(result.length > 0);
461
+ });
462
+
463
+ // processMemoryContent tests
464
+ test('processMemoryContent handles empty and null input', t => {
465
+ t.is(processMemoryContent('', {}), '');
466
+ t.is(processMemoryContent(null, {}), null);
467
+ t.is(processMemoryContent(undefined, {}), undefined);
468
+ });
469
+
470
+ test('processMemoryContent returns unmodified content when no options set', t => {
471
+ const input = '1|2024-03-19T10:00:00Z|Test content';
472
+ t.is(processMemoryContent(input, {}), input);
473
+ t.is(processMemoryContent(input, { priority: 0, recentHours: 0, numResults: 0, stripMetadata: false }), input);
474
+ });
475
+
476
+ test('processMemoryContent filters by priority correctly', t => {
477
+ const input = `
478
+ 1|2024-03-19T10:00:00Z|Priority 1
479
+ 2|2024-03-19T10:00:00Z|Priority 2
480
+ 3|2024-03-19T10:00:00Z|Priority 3
481
+ invalid|2024-03-19T10:00:00Z|Invalid priority
482
+ |2024-03-19T10:00:00Z|Missing priority`.trim();
483
+
484
+ const result = processMemoryContent(input, { priority: 2 });
485
+ t.true(result.includes('Priority 1'));
486
+ t.true(result.includes('Priority 2'));
487
+ t.false(result.includes('Priority 3'));
488
+ t.false(result.includes('Invalid priority'));
489
+ t.false(result.includes('Missing priority'));
490
+ });
491
+
492
+ test('processMemoryContent filters by recency correctly', t => {
493
+ const now = Date.now();
494
+ const hour = 60 * 60 * 1000;
495
+ const recentTime = new Date(now - hour).toISOString();
496
+ const oldTime = new Date(now - 3 * hour).toISOString();
497
+
498
+ const input = `
499
+ 1|${recentTime}|Recent memory
500
+ 2|${oldTime}|Old memory
501
+ 3|invalid_time|Invalid timestamp
502
+ 4||Missing timestamp`.trim();
503
+
504
+ const result = processMemoryContent(input, { recentHours: 2 });
505
+ t.true(result.includes('Recent memory'));
506
+ t.false(result.includes('Old memory'));
507
+ t.false(result.includes('Invalid timestamp'));
508
+ t.false(result.includes('Missing timestamp'));
509
+ });
510
+
511
+ test('processMemoryContent applies numResults limit correctly', t => {
512
+ const input = `
513
+ 1|2024-03-19T10:00:00.000Z|First
514
+ 2|2024-03-19T11:00:00.000Z|Second
515
+ 3|2024-03-19T12:00:00.000Z|Third`.trim();
516
+
517
+ const result = processMemoryContent(input, { numResults: 2 });
518
+ t.true(result.includes('Third')); // Newest
519
+ t.true(result.includes('Second')); // Second newest
520
+ t.false(result.includes('First')); // Oldest
521
+ t.is(result.split('\n').length, 2);
522
+ });
523
+
524
+ test('processMemoryContent strips metadata correctly', t => {
525
+ const input = `
526
+ 1|2024-03-19T10:00:00Z|Normal content
527
+ 2|2024-03-19T11:00:00Z|Content with|pipes|inside
528
+ malformed_line_no_separators
529
+ 3|2024-03-19T12:00:00Z|More|content
530
+ 4|invalid_timestamp|Still valid
531
+ 5||No timestamp but valid
532
+ |2024-03-19T13:00:00Z|No priority
533
+ ||No priority or timestamp`.trim();
534
+
535
+ const result = processMemoryContent(input, { stripMetadata: true });
536
+ t.true(result.includes('Normal content'));
537
+ t.true(result.includes('Content with|pipes|inside'));
538
+ t.true(result.includes('More|content'));
539
+ t.true(result.includes('Still valid'));
540
+ t.true(result.includes('No timestamp but valid'));
541
+ t.false(result.includes('2024-03-19'));
542
+ t.false(result.includes('|2024'));
543
+ // Malformed lines should be preserved as-is
544
+ t.true(result.includes('malformed_line_no_separators'));
545
+ });
546
+
547
+ test('processMemoryContent combines all filters correctly', t => {
548
+ const now = Date.now();
549
+ const hour = 60 * 60 * 1000;
550
+ const recentTime = new Date(now - hour).toISOString();
551
+ const oldTime = new Date(now - 3 * hour).toISOString();
552
+
553
+ const input = `
554
+ 1|${recentTime}|High priority recent
555
+ 2|${recentTime}|Medium priority recent
556
+ 3|${recentTime}|Low priority recent
557
+ 1|${oldTime}|High priority old
558
+ 2|${oldTime}|Medium priority old
559
+ 3|${oldTime}|Low priority old`.trim();
560
+
561
+ const result = processMemoryContent(input, {
562
+ priority: 2,
563
+ recentHours: 2,
564
+ numResults: 2,
565
+ stripMetadata: true
566
+ });
567
+
568
+ // Should only include recent memories with priority 1-2, limited to 2 results
569
+ t.true(result.includes('High priority recent'));
570
+ t.true(result.includes('Medium priority recent'));
571
+ t.false(result.includes('Low priority recent'));
572
+ t.false(result.includes('High priority old'));
573
+ t.false(result.includes('Medium priority old'));
574
+ t.false(result.includes('Low priority old'));
575
+ t.false(result.includes('|')); // Metadata should be stripped
576
+ t.is(result.split('\n').length, 2); // Should only have 2 results
577
+ });
578
+
579
+ test('processMemoryContent handles empty lines and whitespace', t => {
580
+ const input = `
581
+
582
+ 1|2024-03-19T10:00:00Z|Content 1
583
+
584
+ 2|2024-03-19T11:00:00Z|Content 2
585
+
586
+ `.trim();
587
+
588
+ const result = processMemoryContent(input, { stripMetadata: true });
589
+ t.is(result.split('\n').filter(Boolean).length, 2);
590
+ t.true(result.includes('Content 1'));
591
+ t.true(result.includes('Content 2'));
592
+ });
593
+
594
+ test('processMemoryContent handles special characters in content', t => {
595
+ const input = `
596
+ 1|2024-03-19T10:00:00Z|Content with 🚀 emoji
597
+ 2|2024-03-19T11:00:00Z|Content with üñîçødé
598
+ 3|2024-03-19T12:00:00Z|Content with "quotes" and 'apostrophes'
599
+ 4|2024-03-19T13:00:00Z|Content with \n\t\r escape chars`.trim();
600
+
601
+ const result = processMemoryContent(input, { stripMetadata: true });
602
+ t.true(result.includes('Content with 🚀 emoji'));
603
+ t.true(result.includes('Content with üñîçødé'));
604
+ t.true(result.includes('Content with "quotes" and \'apostrophes\''));
605
+ t.true(result.includes('Content with \n\t\r escape chars'));
606
+ });
607
+
608
+ test('processMemoryContent handles extremely long content', t => {
609
+ const longContent = 'a'.repeat(10000);
610
+ const input = `1|2024-03-19T10:00:00Z|${longContent}`;
611
+
612
+ const result = processMemoryContent(input, { stripMetadata: true });
613
+ t.is(result, longContent);
614
+ });
615
+
616
+ test('processMemoryContent priority filtering edge cases', t => {
617
+ const input = `
618
+ 1|2024-03-19T10:00:00Z|Valid priority 1
619
+ 01|2024-03-19T10:00:00Z|Leading zero priority
620
+ 1.5|2024-03-19T10:00:00Z|Decimal priority
621
+ -1|2024-03-19T10:00:00Z|Negative priority
622
+ a|2024-03-19T10:00:00Z|Letter priority
623
+ 9999|2024-03-19T10:00:00Z|Large priority`.trim();
624
+
625
+ const result = processMemoryContent(input, { priority: 2 });
626
+ t.true(result.includes('Valid priority 1'));
627
+ t.false(result.includes('Decimal priority'));
628
+ t.false(result.includes('Negative priority'));
629
+ t.false(result.includes('Letter priority'));
630
+ t.false(result.includes('Large priority'));
631
+ });
632
+
633
+ test('processMemoryContent timestamp filtering edge cases', t => {
634
+ const now = new Date();
635
+ const input = `
636
+ 1|${now.toISOString()}|Valid ISO timestamp
637
+ 1|${now.toUTCString()}|UTC timestamp
638
+ 1|${now.getTime()}|Timestamp as number
639
+ 1|2024-03-19|Partial date
640
+ 1|Invalid Date|Invalid date string
641
+ 1|9999999999999|Future timestamp`.trim();
642
+
643
+ const result = processMemoryContent(input, { recentHours: 1 });
644
+ t.true(result.includes('Valid ISO timestamp'));
645
+ t.false(result.includes('UTC timestamp'));
646
+ t.false(result.includes('Timestamp as number'));
647
+ t.false(result.includes('Partial date'));
648
+ t.false(result.includes('Invalid date string'));
649
+ t.false(result.includes('Future timestamp'));
136
650
  });