@animalabs/connectome-host 0.7.2 → 0.7.4

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 (63) hide show
  1. package/CHANGELOG.md +203 -10
  2. package/HEADLESS-FLEET-PLAN.md +22 -0
  3. package/README.md +22 -11
  4. package/docs/AGENT-ONBOARDING.md +20 -1
  5. package/docs/debug-context-api.md +2 -2
  6. package/docs/retrieval-traces.md +173 -0
  7. package/docs/webui-deployment.md +2 -1
  8. package/package.json +3 -3
  9. package/scripts/audit-module-optins.ts +288 -0
  10. package/scripts/warmup-session.ts +17 -3
  11. package/src/codex-subscription-adapter.ts +13 -1
  12. package/src/framework-agent-config.ts +59 -4
  13. package/src/framework-strategy.ts +33 -3
  14. package/src/headless.ts +14 -0
  15. package/src/index.ts +95 -35
  16. package/src/logging-adapter.ts +13 -2
  17. package/src/mcpl-config.ts +8 -0
  18. package/src/modules/fleet-module.ts +60 -1
  19. package/src/modules/fleet-types.ts +30 -1
  20. package/src/modules/identity-module.ts +274 -0
  21. package/src/modules/mcpl-admin-module.ts +78 -5
  22. package/src/modules/observers-module.ts +12 -0
  23. package/src/modules/retrieval-module.ts +254 -52
  24. package/src/modules/retrieval-trace-page.ts +254 -0
  25. package/src/modules/retrieval-trace.ts +904 -0
  26. package/src/modules/settings-module.ts +28 -2
  27. package/src/modules/subscription-gc-module.ts +54 -1
  28. package/src/modules/tts-relay-module.ts +33 -18
  29. package/src/modules/web-ui-module.ts +445 -894
  30. package/src/recipe.ts +137 -12
  31. package/src/retrieval-config.ts +39 -0
  32. package/src/strategies/frontdesk-strategy.ts +34 -125
  33. package/src/tui.ts +325 -54
  34. package/src/web/panel-data.ts +1187 -0
  35. package/src/web/protocol.ts +75 -10
  36. package/test/audit-module-optins.test.ts +167 -0
  37. package/test/bedrock-prompt-caching.test.ts +170 -0
  38. package/test/fleet-panel-request.test.ts +90 -0
  39. package/test/framework-strategy-defaults.test.ts +110 -0
  40. package/test/frontdesk-strategy.test.ts +25 -37
  41. package/test/headless-panel-request.test.ts +201 -0
  42. package/test/identity-and-surfaces.test.ts +157 -0
  43. package/test/mcpl-admin-module.test.ts +23 -0
  44. package/test/mock-headless-child.ts +14 -0
  45. package/test/retrieval-auth-loopback.test.ts +49 -0
  46. package/test/retrieval-config.test.ts +74 -0
  47. package/test/retrieval-module.test.ts +821 -0
  48. package/test/subscription-gc-module.test.ts +152 -0
  49. package/test/tui-format.test.ts +106 -0
  50. package/test/web-ui-context-coverage.test.ts +1 -1
  51. package/test/web-ui-module.test.ts +189 -3
  52. package/test/web-ui-observers.test.ts +8 -5
  53. package/test/web-ui-protocol.test.ts +0 -0
  54. package/web/bun.lock +345 -0
  55. package/web/src/App.tsx +159 -44
  56. package/web/src/Context.tsx +35 -8
  57. package/web/src/ContextDocument.tsx +20 -5
  58. package/web/src/Files.tsx +2 -8
  59. package/web/src/Lessons.tsx +2 -38
  60. package/web/src/Mcpl.tsx +80 -14
  61. package/web/src/Pins.tsx +5 -0
  62. package/web/src/Settings.tsx +5 -0
  63. package/web/vite.config.ts +8 -2
@@ -0,0 +1,821 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { Membrane, NormalizedRequest } from '@animalabs/membrane';
3
+ import { RetrievalModule } from '../src/modules/retrieval-module.js';
4
+ import { RetrievalTraceStore } from '../src/modules/retrieval-trace.js';
5
+ import { buildRetrievalModuleConfig } from '../src/retrieval-config.js';
6
+ import type { Lesson } from '../src/modules/lessons-module.js';
7
+
8
+ const TEST_AGENT = 'test-agent';
9
+ const TEST_RETRIEVAL_MODEL = 'test-retrieval-model';
10
+
11
+ function lesson(id: string, content: string): Lesson {
12
+ return {
13
+ id,
14
+ content,
15
+ confidence: 0.9,
16
+ tags: ['memory'],
17
+ evidence: [],
18
+ created: 1,
19
+ updated: 1,
20
+ deprecated: false,
21
+ };
22
+ }
23
+
24
+ function harness(responses: Array<string | Error>, lessons: Lesson[]) {
25
+ const calls: NormalizedRequest[] = [];
26
+ const membrane = {
27
+ complete: async (request: NormalizedRequest) => {
28
+ calls.push(structuredClone(request));
29
+ const next = responses.shift();
30
+ if (next instanceof Error) throw next;
31
+ if (next === undefined) throw new Error('unexpected retrieval call');
32
+ return { content: [{ type: 'text', text: next }] };
33
+ },
34
+ } as unknown as Membrane;
35
+
36
+ const installContext = (mod: RetrievalModule) => {
37
+ (mod as unknown as { ctx: unknown }).ctx = {
38
+ getModule: (name: string) => name === 'lessons' ? { getLessons: () => lessons } : null,
39
+ queryMessages: () => ({
40
+ messages: [{
41
+ participant: 'user',
42
+ content: [{ type: 'text', text: 'Please recall the relevant memory context.' }],
43
+ }],
44
+ totalCount: 1,
45
+ }),
46
+ };
47
+ };
48
+
49
+ return { calls, membrane, installContext };
50
+ }
51
+
52
+ describe('RetrievalModule provider-specific reasoning', () => {
53
+ test('passes configured reasoning to concept extraction and relevance validation', async () => {
54
+ const lessons = [1, 2, 3, 4].map(n => lesson(`l${n}`, `memory detail ${n}`));
55
+ const h = harness(['["memory"]', '["l1", "l3"]'], lessons);
56
+ const mod = new RetrievalModule(buildRetrievalModuleConfig(h.membrane, {
57
+ model: TEST_RETRIEVAL_MODEL,
58
+ reasoningEffort: 'xhigh',
59
+ }, 'openai-codex'));
60
+ h.installContext(mod);
61
+
62
+ const injections = await mod.gatherContext(TEST_AGENT);
63
+
64
+ expect(h.calls).toHaveLength(2);
65
+ for (const request of h.calls) {
66
+ expect(request.config.model).toBe(TEST_RETRIEVAL_MODEL);
67
+ expect(request.providerParams).toEqual({
68
+ reasoning: { effort: 'xhigh' },
69
+ });
70
+ }
71
+ const text = (injections[0].content[0] as { type: 'text'; text: string }).text;
72
+ expect(text).toContain('memory detail 1');
73
+ expect(text).toContain('memory detail 3');
74
+ expect(text).not.toContain('memory detail 2');
75
+ });
76
+
77
+ test('omits providerParams when retrieval reasoning is not configured', async () => {
78
+ const h = harness(['[]'], [lesson('l1', 'memory detail')]);
79
+ const mod = new RetrievalModule({ membrane: h.membrane, retrievalModel: 'gpt-5.4-mini' });
80
+ h.installContext(mod);
81
+
82
+ expect(await mod.gatherContext(TEST_AGENT)).toEqual([]);
83
+ expect(h.calls).toHaveLength(1);
84
+ expect(h.calls[0].providerParams).toBeUndefined();
85
+ });
86
+
87
+ test('skips relevance validation for three or fewer candidates and caches non-empty results', async () => {
88
+ const h = harness(['["memory"]'], [lesson('l1', 'memory alpha'), lesson('l2', 'memory beta')]);
89
+ const mod = new RetrievalModule({
90
+ membrane: h.membrane,
91
+ retrievalModel: TEST_RETRIEVAL_MODEL,
92
+ retrievalReasoning: { effort: 'xhigh' },
93
+ });
94
+ h.installContext(mod);
95
+
96
+ const first = await mod.gatherContext(TEST_AGENT);
97
+ const second = await mod.gatherContext(TEST_AGENT);
98
+
99
+ expect(first).toHaveLength(1);
100
+ expect(second).toEqual(first);
101
+ expect(h.calls).toHaveLength(1);
102
+ expect(h.calls[0].providerParams).toEqual({ reasoning: { effort: 'xhigh' } });
103
+ });
104
+
105
+ test('fails open when the concept extraction provider call fails', async () => {
106
+ const h = harness([new Error('provider unavailable')], [lesson('l1', 'memory detail')]);
107
+ const mod = new RetrievalModule({
108
+ membrane: h.membrane,
109
+ retrievalModel: TEST_RETRIEVAL_MODEL,
110
+ retrievalReasoning: { effort: 'xhigh' },
111
+ });
112
+ h.installContext(mod);
113
+
114
+ expect(await mod.gatherContext(TEST_AGENT)).toEqual([]);
115
+ expect(h.calls).toHaveLength(1);
116
+ expect(mod.getRetrievalTraces()[0]).toMatchObject({
117
+ outcome: 'error',
118
+ error: 'provider unavailable',
119
+ conceptExtraction: { error: 'provider unavailable' },
120
+ });
121
+ });
122
+
123
+ test('fails open and records the relevance stage when its provider call fails', async () => {
124
+ const lessons = [1, 2, 3, 4].map(n => lesson(`l${n}`, `memory detail ${n}`));
125
+ const h = harness(['["memory"]', new Error('relevance unavailable')], lessons);
126
+ const mod = new RetrievalModule({
127
+ membrane: h.membrane,
128
+ retrievalModel: TEST_RETRIEVAL_MODEL,
129
+ retrievalReasoning: { effort: 'high' },
130
+ });
131
+ h.installContext(mod);
132
+
133
+ expect(await mod.gatherContext(TEST_AGENT)).toEqual([]);
134
+ expect(h.calls).toHaveLength(2);
135
+ expect(mod.getRetrievalTraces()[0]).toMatchObject({
136
+ outcome: 'error',
137
+ error: 'relevance unavailable',
138
+ relevance: { ran: true, error: 'relevance unavailable' },
139
+ });
140
+ });
141
+ });
142
+
143
+ describe('RetrievalModule observability', () => {
144
+ test('records a completed error trace and rethrows when getModule throws', async () => {
145
+ const mod = new RetrievalModule({ membrane: {} as Membrane });
146
+ (mod as unknown as { ctx: unknown }).ctx = {
147
+ getModule: () => { throw new Error('getModule failed'); },
148
+ };
149
+
150
+ await expect(mod.gatherContext(TEST_AGENT)).rejects.toThrow('getModule failed');
151
+ expect(mod.getRetrievalTraces()[0]).toMatchObject({
152
+ outcome: 'error',
153
+ error: 'getModule failed',
154
+ });
155
+ expect(mod.getRetrievalTraces()[0].completedAt).toBeDefined();
156
+ });
157
+
158
+ test('records a completed error trace and rethrows when getLessons throws', async () => {
159
+ const mod = new RetrievalModule({ membrane: {} as Membrane });
160
+ (mod as unknown as { ctx: unknown }).ctx = {
161
+ getModule: () => ({ getLessons: () => { throw new Error('getLessons failed'); } }),
162
+ };
163
+
164
+ await expect(mod.gatherContext(TEST_AGENT)).rejects.toThrow('getLessons failed');
165
+ expect(mod.getRetrievalTraces()[0]).toMatchObject({
166
+ outcome: 'error',
167
+ error: 'getLessons failed',
168
+ });
169
+ expect(mod.getRetrievalTraces()[0].completedAt).toBeDefined();
170
+ });
171
+
172
+ test('records a completed error trace and rethrows when queryMessages throws', async () => {
173
+ const mod = new RetrievalModule({ membrane: {} as Membrane });
174
+ (mod as unknown as { ctx: unknown }).ctx = {
175
+ getModule: () => ({ getLessons: () => [lesson('l1', 'memory detail')] }),
176
+ queryMessages: () => { throw new Error('queryMessages failed'); },
177
+ };
178
+
179
+ await expect(mod.gatherContext(TEST_AGENT)).rejects.toThrow('queryMessages failed');
180
+ expect(mod.getRetrievalTraces()[0]).toMatchObject({
181
+ outcome: 'error',
182
+ error: 'queryMessages failed',
183
+ });
184
+ expect(mod.getRetrievalTraces()[0].completedAt).toBeDefined();
185
+ });
186
+
187
+ test('records candidates, selector outputs, selected IDs, and the exact injection', async () => {
188
+ const lessons = [1, 2, 3, 4].map(n => lesson(`l${n}`, `memory detail ${n}`));
189
+ const h = harness(['["memory"]', '["l1", "l3"]'], lessons);
190
+ const mod = new RetrievalModule(buildRetrievalModuleConfig(h.membrane, {
191
+ model: TEST_RETRIEVAL_MODEL,
192
+ maxInjected: 5,
193
+ reasoningEffort: 'xhigh',
194
+ }, 'openai-codex'));
195
+ h.installContext(mod);
196
+
197
+ const injections = await mod.gatherContext(TEST_AGENT);
198
+
199
+ const [trace] = mod.getRetrievalTraces({ includeInputs: true });
200
+ expect(trace.outcome).toBe('injected');
201
+ expect(trace.agentName).toBe(TEST_AGENT);
202
+ expect(trace.config).toMatchObject({
203
+ model: TEST_RETRIEVAL_MODEL,
204
+ requestedReasoning: { effort: 'xhigh' },
205
+ providerParams: { reasoning: { effort: 'xhigh' } },
206
+ maxInjectedLessons: 5,
207
+ });
208
+ expect(trace.context?.input).toContain('Please recall the relevant memory context.');
209
+ expect(trace.conceptExtraction).toMatchObject({
210
+ rawOutput: '["memory"]',
211
+ responseContent: [{ type: 'text', text: '["memory"]' }],
212
+ parsedValues: ['memory'],
213
+ parseMode: 'json',
214
+ });
215
+ expect(trace.candidates.map(candidate => candidate.id)).toEqual(['l1', 'l2', 'l3', 'l4']);
216
+ expect(trace.candidates[0].matches).toContainEqual({
217
+ concept: 'memory', keyword: 'memory', field: 'content',
218
+ });
219
+ expect(trace.relevance).toMatchObject({
220
+ ran: true,
221
+ rawOutput: '["l1", "l3"]',
222
+ responseContent: [{ type: 'text', text: '["l1", "l3"]' }],
223
+ parsedValues: ['l1', 'l3'],
224
+ parseMode: 'json',
225
+ });
226
+ expect(trace.relevantLessonIds).toEqual(['l1', 'l3']);
227
+ expect(trace.injected.lessonIds).toEqual(['l1', 'l3']);
228
+ expect(trace.injected.namespace).toBe(injections[0].namespace);
229
+ expect(trace.injected.position).toBe(injections[0].position);
230
+ expect(trace.injected.block).toBe((injections[0].content[0] as { type: 'text'; text: string }).text);
231
+ expect(trace.injected.block).toContain('## Retrieved Knowledge');
232
+ expect(trace.injected.block).toContain('memory detail 1');
233
+ expect(trace.injected.block).not.toContain('memory detail 2');
234
+
235
+ const [safeView] = mod.getRetrievalTraces();
236
+ expect(safeView.context?.input).toBeUndefined();
237
+ expect(safeView.conceptExtraction?.input).toBeUndefined();
238
+ expect(safeView.relevance?.input).toBeUndefined();
239
+ expect(safeView.candidates[0].content).toBe('memory detail 1');
240
+ });
241
+
242
+ test('retains complete point-in-time lesson snapshots', async () => {
243
+ const source: Lesson = {
244
+ id: 'complete-lesson',
245
+ content: 'memory detail',
246
+ confidence: 0.87,
247
+ tags: ['memory', 'original-tag'],
248
+ evidence: ['message-1', 'message-2'],
249
+ created: 101,
250
+ updated: 202,
251
+ deprecated: false,
252
+ deprecationReason: 'retained optional field',
253
+ };
254
+ const expected = structuredClone(source);
255
+ const h = harness(['["memory"]'], [source]);
256
+ const mod = new RetrievalModule({ membrane: h.membrane });
257
+ h.installContext(mod);
258
+
259
+ expect(await mod.gatherContext(TEST_AGENT)).toHaveLength(1);
260
+ source.content = 'mutated content';
261
+ source.tags.push('mutated-tag');
262
+ source.evidence.push('mutated-evidence');
263
+ source.updated = 999;
264
+ source.deprecationReason = 'mutated reason';
265
+
266
+ const trace = mod.getRetrievalTraces()[0];
267
+ expect(trace.candidates[0]).toMatchObject(expected);
268
+ expect(trace.injected.lessons[0]).toEqual(expected);
269
+
270
+ trace.injected.lessons[0].tags.push('caller mutation');
271
+ trace.injected.lessons[0].evidence.push('caller mutation');
272
+ expect(mod.getRetrievalTraces()[0].injected.lessons[0]).toEqual(expected);
273
+ });
274
+
275
+ test('records cache reuse with a source trace and no extra model calls', async () => {
276
+ const lessons = [1, 2, 3, 4].map(n => lesson(`l${n}`, `memory detail ${n}`));
277
+ const h = harness(['["memory"]', '["l2"]'], lessons);
278
+ const mod = new RetrievalModule({ membrane: h.membrane, retrievalModel: TEST_RETRIEVAL_MODEL });
279
+ h.installContext(mod);
280
+
281
+ await mod.gatherContext(TEST_AGENT);
282
+ await mod.gatherContext(TEST_AGENT);
283
+
284
+ expect(h.calls).toHaveLength(2);
285
+ const [cached, source] = mod.getRetrievalTraces({ limit: 2 });
286
+ expect(source.outcome).toBe('injected');
287
+ expect(cached.outcome).toBe('cache-hit');
288
+ expect(cached.cache).toEqual({ hit: true, sourceTraceId: source.id });
289
+ expect(cached.injected.lessonIds).toEqual(['l2']);
290
+ expect(cached.injected.lessons).toEqual(source.injected.lessons);
291
+ expect(cached.injected.lessons[0].content).toBe('memory detail 2');
292
+ expect(cached.injected.block).toContain('memory detail 2');
293
+ });
294
+
295
+ test('cache-hit snapshots stay complete after source eviction', async () => {
296
+ const source: Lesson = {
297
+ ...lesson('l1', 'memory detail'),
298
+ evidence: ['message-1'],
299
+ created: 10,
300
+ updated: 20,
301
+ deprecationReason: 'optional metadata',
302
+ };
303
+ const expected = structuredClone(source);
304
+ const h = harness(['["memory"]'], [source]);
305
+ const mod = new RetrievalModule({ membrane: h.membrane });
306
+ h.installContext(mod);
307
+
308
+ await mod.gatherContext(TEST_AGENT);
309
+ for (let i = 0; i < 105; i++) await mod.gatherContext(TEST_AGENT);
310
+
311
+ const cached = mod.getRetrievalTraces({ limit: 100 })[0];
312
+ expect(cached.outcome).toBe('cache-hit');
313
+ expect(cached.cache).toEqual({ hit: true, sourceTraceEvicted: true });
314
+ expect(cached.injected.lessons[0]).toEqual(expected);
315
+ });
316
+
317
+ test('records skipped validation and relevance fallback without changing behavior', async () => {
318
+ const few = harness(['["memory"]'], [lesson('l1', 'memory one'), lesson('l2', 'memory two')]);
319
+ const fewMod = new RetrievalModule({ membrane: few.membrane });
320
+ few.installContext(fewMod);
321
+ await fewMod.gatherContext(TEST_AGENT);
322
+ expect(fewMod.getRetrievalTraces()[0].relevance).toMatchObject({
323
+ ran: false,
324
+ skippedReason: 'three-or-fewer-candidates',
325
+ });
326
+ expect(fewMod.getRetrievalTraces()[0].relevance?.parsedValues).toBeUndefined();
327
+
328
+ const manyLessons = [1, 2, 3, 4].map(n => lesson(`l${n}`, `memory ${n}`));
329
+ const many = harness(['["memory"]', 'not valid json'], manyLessons);
330
+ const manyMod = new RetrievalModule({ membrane: many.membrane });
331
+ many.installContext(manyMod);
332
+ const injections = await manyMod.gatherContext(TEST_AGENT);
333
+ expect(injections).toHaveLength(1);
334
+ expect(manyMod.getRetrievalTraces()[0].relevance).toMatchObject({
335
+ ran: true,
336
+ rawOutput: 'not valid json',
337
+ parsedValues: [],
338
+ parseMode: 'fallback',
339
+ });
340
+ expect(manyMod.getRetrievalTraces()[0].relevantLessonIds).toEqual(['l1', 'l2', 'l3', 'l4']);
341
+ });
342
+
343
+ test('trace-only message IDs cannot make retrieval fail', async () => {
344
+ const h = harness(['["memory"]'], [lesson('l1', 'memory detail')]);
345
+ const mod = new RetrievalModule({ membrane: h.membrane });
346
+ const message = {
347
+ participant: 'user',
348
+ content: [{ type: 'text', text: 'memory please' }],
349
+ } as Record<string, unknown>;
350
+ Object.defineProperty(message, 'id', { get: () => { throw new Error('trace-only id getter'); } });
351
+ (mod as unknown as { ctx: unknown }).ctx = {
352
+ getModule: (name: string) => name === 'lessons' ? { getLessons: () => [lesson('l1', 'memory detail')] } : null,
353
+ queryMessages: () => ({ messages: [message], totalCount: 1 }),
354
+ };
355
+
356
+ const injections = await mod.gatherContext(TEST_AGENT);
357
+ expect(injections).toHaveLength(1);
358
+ expect(mod.getRetrievalTraces({ includeInputs: true })[0].context?.messageIds).toEqual([]);
359
+ });
360
+
361
+ test('malformed mixed wrapper retains historical fail-open behavior', async () => {
362
+ const h = harness(['prose ["memory", 3]'], [lesson('l1', 'memory detail')]);
363
+ const mod = new RetrievalModule({ membrane: h.membrane });
364
+ h.installContext(mod);
365
+
366
+ expect(await mod.gatherContext(TEST_AGENT)).toEqual([]);
367
+ const [trace] = mod.getRetrievalTraces();
368
+ expect(trace.outcome).toBe('error');
369
+ expect(trace.conceptExtraction).toMatchObject({
370
+ parseMode: 'array-extraction',
371
+ parsedValues: ['memory'],
372
+ });
373
+ });
374
+
375
+ test('candidate provenance mirrors historical empty-keyword matching', async () => {
376
+ const h = harness(['[" "]'], [lesson('l1', 'unrelated detail')]);
377
+ const mod = new RetrievalModule({ membrane: h.membrane });
378
+ h.installContext(mod);
379
+
380
+ expect(await mod.gatherContext(TEST_AGENT)).toHaveLength(1);
381
+ expect(mod.getRetrievalTraces()[0].candidates[0].matches).toContainEqual({
382
+ concept: ' ', keyword: '', field: 'content',
383
+ });
384
+ });
385
+
386
+ test('provider blocks preserve JSON safety markers for unusual values', async () => {
387
+ const opaque: Record<string, unknown> = {
388
+ type: 'redacted_thinking',
389
+ bytes: 42n,
390
+ nonfinite: Number.POSITIVE_INFINITY,
391
+ };
392
+ opaque.self = opaque;
393
+ Object.defineProperty(opaque, 'unreadable', {
394
+ enumerable: true,
395
+ get: () => { throw new Error('unreadable provider property'); },
396
+ });
397
+ const membrane = {
398
+ complete: async () => ({
399
+ content: [{ type: 'text', text: '["memory"]' }, opaque],
400
+ }),
401
+ } as unknown as Membrane;
402
+ const mod = new RetrievalModule({ membrane });
403
+ (mod as unknown as { ctx: unknown }).ctx = {
404
+ getModule: (name: string) => name === 'lessons' ? { getLessons: () => [lesson('l1', 'memory detail')] } : null,
405
+ queryMessages: () => ({
406
+ messages: [{ participant: 'user', content: [{ type: 'text', text: 'memory' }] }],
407
+ totalCount: 1,
408
+ }),
409
+ };
410
+
411
+ expect(await mod.gatherContext(TEST_AGENT)).toHaveLength(1);
412
+ const serialized = JSON.stringify(mod.getRetrievalTraces({ includeInputs: true }));
413
+ expect(serialized).toContain('"type":"bigint"');
414
+ expect(serialized).toContain('"type":"circular"');
415
+ expect(serialized).toContain('"type":"number"');
416
+ expect(serialized).toContain('"type":"unreadable"');
417
+ });
418
+
419
+ test('canonicalizes non-JSON provider parameters before byte accounting', () => {
420
+ const binary = new ArrayBuffer(2 * 1024 * 1024);
421
+ const broad = Object.fromEntries(Array.from({ length: 100 }, (_, i) => [`key-${i}`, i]));
422
+ const store = new RetrievalTraceStore({ byteBudget: 4096 });
423
+ const run = store.begin({
424
+ agentName: TEST_AGENT,
425
+ model: TEST_RETRIEVAL_MODEL,
426
+ providerParams: {
427
+ binary,
428
+ view: new Uint8Array(binary),
429
+ map: new Map([['binary', binary]]),
430
+ set: new Set([binary]),
431
+ broad,
432
+ } as unknown as Record<string, unknown>,
433
+ minConfidence: 0.3,
434
+ maxCandidates: 20,
435
+ maxInjectedLessons: 5,
436
+ });
437
+ run.finish('no-concepts');
438
+
439
+ const [trace] = store.list({ includeInputs: true });
440
+ expect(trace.truncation).toBeUndefined();
441
+ expect(trace.config.providerParams).toMatchObject({
442
+ binary: { type: 'array-buffer', byteLength: binary.byteLength, unavailable: true },
443
+ view: { type: 'array-buffer-view', byteLength: binary.byteLength, unavailable: true },
444
+ map: { type: 'map', size: 1, unavailable: true },
445
+ set: { type: 'set', size: 1, unavailable: true },
446
+ });
447
+ expect(trace.config.providerParamsTruncation?.reasons).toContain('non-json-value');
448
+ expect(trace.config.providerParamsTruncation?.reasons).toContain('max-object-keys');
449
+ expect(store.retainedBytes).toBeLessThanOrEqual(4096);
450
+ });
451
+
452
+ test('uses intrinsic primitive metadata for shadowed binary and collection values', () => {
453
+ const hiddenMetadata = new ArrayBuffer(2 * 1024 * 1024);
454
+ const binary = new ArrayBuffer(8);
455
+ const typedArray = new Uint8Array(16);
456
+ const dataView = new DataView(new ArrayBuffer(24));
457
+ const map = new Map([['value', 1]]);
458
+ const set = new Set(['value']);
459
+ Object.defineProperty(binary, 'byteLength', { value: hiddenMetadata });
460
+ Object.defineProperty(typedArray, 'byteLength', { value: hiddenMetadata });
461
+ Object.defineProperty(dataView, 'byteLength', { value: hiddenMetadata });
462
+ Object.defineProperty(map, 'size', { value: hiddenMetadata });
463
+ Object.defineProperty(set, 'size', { value: hiddenMetadata });
464
+
465
+ const byteBudget = 4096;
466
+ const store = new RetrievalTraceStore({ byteBudget });
467
+ const run = store.begin({
468
+ agentName: TEST_AGENT,
469
+ model: TEST_RETRIEVAL_MODEL,
470
+ providerParams: { binary, typedArray, dataView, map, set },
471
+ minConfidence: 0.3,
472
+ maxCandidates: 20,
473
+ maxInjectedLessons: 5,
474
+ });
475
+ run.finish('no-concepts');
476
+
477
+ const traces = store.list({ includeInputs: true });
478
+ const [trace] = traces;
479
+ expect(trace.truncation).toBeUndefined();
480
+ expect(trace.config.providerParams).toMatchObject({
481
+ binary: { type: 'array-buffer', byteLength: 8, unavailable: true },
482
+ typedArray: { type: 'array-buffer-view', byteLength: 16, unavailable: true },
483
+ dataView: { type: 'array-buffer-view', byteLength: 24, unavailable: true },
484
+ map: { type: 'map', size: 1, unavailable: true },
485
+ set: { type: 'set', size: 1, unavailable: true },
486
+ });
487
+ expect(trace.config.providerParamsTruncation?.reasons).toEqual(['non-json-value']);
488
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
489
+ expect(new TextEncoder().encode(JSON.stringify(traces)).byteLength)
490
+ .toBeLessThanOrEqual(byteBudget);
491
+ });
492
+
493
+ test('bounds oversized BigInt decimal output with truthful string-limit reasons', () => {
494
+ const byteBudget = 32 * 1024;
495
+ const store = new RetrievalTraceStore({ byteBudget });
496
+ const run = store.begin({
497
+ agentName: TEST_AGENT,
498
+ model: TEST_RETRIEVAL_MODEL,
499
+ providerParams: { hugeInteger: 1n << 600_000n },
500
+ minConfidence: 0.3,
501
+ maxCandidates: 20,
502
+ maxInjectedLessons: 5,
503
+ });
504
+ run.finish('no-concepts');
505
+
506
+ const traces = store.list({ includeInputs: true });
507
+ const [trace] = traces;
508
+ const marker = trace.config.providerParams?.hugeInteger as {
509
+ type: string;
510
+ value: string;
511
+ unavailable: boolean;
512
+ };
513
+ expect(trace.truncation).toBeUndefined();
514
+ expect(marker.type).toBe('bigint');
515
+ expect(marker.unavailable).toBe(true);
516
+ expect(marker.value.endsWith('...[truncated]')).toBe(true);
517
+ expect(new TextEncoder().encode(marker.value).byteLength).toBeLessThanOrEqual(16 * 1024);
518
+ expect(trace.config.providerParamsTruncation?.reasons).toContain('max-string-bytes');
519
+ expect(trace.config.providerParamsTruncation?.reasons).toContain('max-total-string-bytes');
520
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
521
+ expect(new TextEncoder().encode(JSON.stringify(traces)).byteLength)
522
+ .toBeLessThanOrEqual(byteBudget);
523
+ });
524
+
525
+ test('bounds non-string Error.message values before retention', () => {
526
+ const hiddenMessage = new ArrayBuffer(2 * 1024 * 1024);
527
+ const error = new Error('ordinary');
528
+ Object.defineProperty(error, 'message', { value: hiddenMessage });
529
+ const byteBudget = 1024;
530
+ const store = new RetrievalTraceStore({ byteBudget });
531
+ const run = store.begin({
532
+ agentName: TEST_AGENT,
533
+ model: TEST_RETRIEVAL_MODEL,
534
+ minConfidence: 0.3,
535
+ maxCandidates: 20,
536
+ maxInjectedLessons: 5,
537
+ });
538
+ run.finish('error', error);
539
+
540
+ const traces = store.list({ includeInputs: true });
541
+ const [trace] = traces;
542
+ expect(trace.truncation).toBeUndefined();
543
+ expect(trace.error).toBe('[object ArrayBuffer]');
544
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
545
+ expect(new TextEncoder().encode(JSON.stringify(traces)).byteLength)
546
+ .toBeLessThanOrEqual(byteBudget);
547
+ });
548
+
549
+ test('retains enumerable __proto__ input as an accounted own data property', () => {
550
+ const providerParams: Record<string, unknown> = {};
551
+ Object.defineProperty(providerParams, '__proto__', {
552
+ value: 1n << 600_000n,
553
+ enumerable: true,
554
+ });
555
+ const byteBudget = 1024;
556
+ const store = new RetrievalTraceStore({ byteBudget });
557
+ const run = store.begin({
558
+ agentName: TEST_AGENT,
559
+ model: TEST_RETRIEVAL_MODEL,
560
+ providerParams,
561
+ minConfidence: 0.3,
562
+ maxCandidates: 20,
563
+ maxInjectedLessons: 5,
564
+ });
565
+ run.finish('no-concepts');
566
+
567
+ const traces = store.list({ includeInputs: true });
568
+ const [trace] = traces;
569
+ expect(trace.truncation).toMatchObject({
570
+ kind: 'tombstone',
571
+ reason: 'trace-exceeded-byte-budget',
572
+ byteBudget,
573
+ });
574
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
575
+ expect(new TextEncoder().encode(JSON.stringify(traces)).byteLength)
576
+ .toBeLessThanOrEqual(byteBudget);
577
+ });
578
+
579
+ test('uses intrinsic bounded Date output and fails safe for invalid dates', () => {
580
+ let customCalled = false;
581
+ const validDate = new Date('2026-07-31T12:34:56.789Z');
582
+ Object.defineProperty(validDate, 'toISOString', {
583
+ value: () => {
584
+ customCalled = true;
585
+ return 'x'.repeat(2 * 1024 * 1024);
586
+ },
587
+ });
588
+ const invalidDate = new Date(Number.NaN);
589
+ Object.defineProperty(invalidDate, 'toISOString', {
590
+ value: () => new ArrayBuffer(2 * 1024 * 1024),
591
+ });
592
+
593
+ const byteBudget = 4096;
594
+ const store = new RetrievalTraceStore({ byteBudget });
595
+ const run = store.begin({
596
+ agentName: TEST_AGENT,
597
+ model: TEST_RETRIEVAL_MODEL,
598
+ providerParams: { validDate, invalidDate },
599
+ minConfidence: 0.3,
600
+ maxCandidates: 20,
601
+ maxInjectedLessons: 5,
602
+ });
603
+ run.finish('no-concepts');
604
+
605
+ const traces = store.list({ includeInputs: true });
606
+ const [trace] = traces;
607
+ expect(customCalled).toBe(false);
608
+ expect(trace.truncation).toBeUndefined();
609
+ expect(trace.config.providerParams).toMatchObject({
610
+ validDate: '2026-07-31T12:34:56.789Z',
611
+ invalidDate: { type: 'date', unavailable: true },
612
+ });
613
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
614
+ expect(new TextEncoder().encode(JSON.stringify(traces)).byteLength)
615
+ .toBeLessThanOrEqual(byteBudget);
616
+ });
617
+
618
+ test('provider block snapshots bound breadth and strings with explicit reasons', async () => {
619
+ const opaque = {
620
+ type: 'opaque',
621
+ huge: '🔒'.repeat(50_000),
622
+ items: Array.from({ length: 10_000 }, () => 'x'.repeat(4_000)),
623
+ };
624
+ const membrane = {
625
+ complete: async () => ({
626
+ content: [{ type: 'text', text: '["memory"]' }, opaque],
627
+ }),
628
+ } as unknown as Membrane;
629
+ const mod = new RetrievalModule({ membrane });
630
+ (mod as unknown as { ctx: unknown }).ctx = {
631
+ getModule: () => ({ getLessons: () => [lesson('l1', 'memory detail')] }),
632
+ queryMessages: () => ({
633
+ messages: [{ participant: 'user', content: [{ type: 'text', text: 'memory' }] }],
634
+ totalCount: 1,
635
+ }),
636
+ };
637
+
638
+ expect(await mod.gatherContext(TEST_AGENT)).toHaveLength(1);
639
+ const stage = mod.getRetrievalTraces({ includeInputs: true })[0].conceptExtraction;
640
+ expect(stage?.responseContentTruncation?.truncated).toBe(true);
641
+ expect(stage?.responseContentTruncation?.reasons).toContain('max-array-items');
642
+ expect(stage?.responseContentTruncation?.reasons).toContain('max-string-bytes');
643
+ expect(stage?.responseContentTruncation?.reasons).toContain('max-total-string-bytes');
644
+ expect(new TextEncoder().encode(JSON.stringify(stage?.responseContent)).byteLength)
645
+ .toBeLessThan(256 * 1024);
646
+ });
647
+
648
+ test('in-flight retrieval is visible before the provider returns', async () => {
649
+ let release!: (value: unknown) => void;
650
+ const membrane = {
651
+ complete: () => new Promise(resolve => { release = resolve; }),
652
+ } as unknown as Membrane;
653
+ const mod = new RetrievalModule({ membrane });
654
+ (mod as unknown as { ctx: unknown }).ctx = {
655
+ getModule: (name: string) => name === 'lessons' ? { getLessons: () => [lesson('l1', 'memory detail')] } : null,
656
+ queryMessages: () => ({
657
+ messages: [{ participant: 'user', content: [{ type: 'text', text: 'memory' }] }],
658
+ totalCount: 1,
659
+ }),
660
+ };
661
+
662
+ const pending = mod.gatherContext(TEST_AGENT);
663
+ await new Promise(resolve => setTimeout(resolve, 0));
664
+ const [running] = mod.getRetrievalTraces({ includeInputs: true });
665
+ expect(running.outcome).toBeUndefined();
666
+ expect(running.completedAt).toBeUndefined();
667
+ expect(running.conceptExtraction?.input).toContain('memory');
668
+
669
+ release({ content: [{ type: 'text', text: '["memory"]' }] });
670
+ expect(await pending).toHaveLength(1);
671
+ expect(mod.getRetrievalTraces()[0].outcome).toBe('injected');
672
+ });
673
+
674
+ test('cache links are marked evicted rather than left dangling', async () => {
675
+ const h = harness(['["memory"]'], [lesson('l1', 'memory detail')]);
676
+ const mod = new RetrievalModule({ membrane: h.membrane });
677
+ h.installContext(mod);
678
+
679
+ await mod.gatherContext(TEST_AGENT);
680
+ for (let i = 0; i < 105; i++) await mod.gatherContext(TEST_AGENT);
681
+
682
+ const traces = mod.getRetrievalTraces({ limit: 100 });
683
+ expect(traces).toHaveLength(100);
684
+ expect(traces.every(trace => trace.outcome === 'cache-hit')).toBe(true);
685
+ expect(traces.every(trace => trace.cache.sourceTraceId === undefined)).toBe(true);
686
+ expect(traces.every(trace => trace.cache.sourceTraceEvicted === true)).toBe(true);
687
+ });
688
+
689
+ test('preserves the numeric capacity constructor contract', () => {
690
+ const store = new RetrievalTraceStore(2);
691
+ for (let i = 0; i < 3; i++) {
692
+ const run = store.begin({
693
+ agentName: TEST_AGENT,
694
+ model: TEST_RETRIEVAL_MODEL,
695
+ minConfidence: 0.3,
696
+ maxCandidates: 20,
697
+ maxInjectedLessons: 5,
698
+ });
699
+ run.finish('not-started');
700
+ }
701
+ expect(store.list({ limit: 100 }).map(trace => trace.id)).toEqual([3, 2]);
702
+ });
703
+
704
+ test('retains only the newest 100 runs', async () => {
705
+ const mod = new RetrievalModule({ membrane: {} as Membrane });
706
+ for (let i = 0; i < 105; i++) await mod.gatherContext(TEST_AGENT);
707
+ const traces = mod.getRetrievalTraces({ limit: 100 });
708
+ expect(traces).toHaveLength(100);
709
+ expect(traces[0].id).toBe(105);
710
+ expect(traces.at(-1)?.id).toBe(6);
711
+ expect(traces.every(trace => trace.outcome === 'not-started')).toBe(true);
712
+ });
713
+
714
+ test('evicts oldest traces to stay within the UTF-8 byte budget', () => {
715
+ const byteBudget = 2800;
716
+ const store = new RetrievalTraceStore({ byteBudget });
717
+ for (let i = 0; i < 3; i++) {
718
+ const run = store.begin({
719
+ agentName: TEST_AGENT,
720
+ model: TEST_RETRIEVAL_MODEL,
721
+ minConfidence: 0.3,
722
+ maxCandidates: 20,
723
+ maxInjectedLessons: 5,
724
+ });
725
+ run.setContext(`hash-${i}`, `context-${i}-` + 'x'.repeat(1300), 1, [`m${i}`]);
726
+ run.finish('no-concepts');
727
+ }
728
+
729
+ const traces = store.list({ limit: 100, includeInputs: true });
730
+ expect(traces.length).toBeLessThan(3);
731
+ expect(traces[0].id).toBe(3);
732
+ expect(traces[0].truncation).toBeUndefined();
733
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
734
+ expect(new TextEncoder().encode(JSON.stringify(traces)).byteLength)
735
+ .toBeLessThanOrEqual(byteBudget);
736
+ });
737
+
738
+ test('marks a byte-tombstoned cache source honestly', () => {
739
+ const byteBudget = 4096;
740
+ const store = new RetrievalTraceStore({ byteBudget });
741
+ const source = store.begin({
742
+ agentName: TEST_AGENT,
743
+ model: TEST_RETRIEVAL_MODEL,
744
+ minConfidence: 0.3,
745
+ maxCandidates: 20,
746
+ maxInjectedLessons: 5,
747
+ });
748
+ source.setContext('large-source', 'x'.repeat(20_000), 1, ['message-1']);
749
+ source.finish('injected');
750
+
751
+ const cached = store.begin({
752
+ agentName: TEST_AGENT,
753
+ model: TEST_RETRIEVAL_MODEL,
754
+ minConfidence: 0.3,
755
+ maxCandidates: 20,
756
+ maxInjectedLessons: 5,
757
+ });
758
+ cached.recordCacheHit(source.id, ['l1'], [lesson('l1', 'memory detail')], []);
759
+ cached.finish('cache-hit');
760
+
761
+ const [cacheTrace, sourceTrace] = store.list({ limit: 2 });
762
+ expect(sourceTrace.truncation?.kind).toBe('tombstone');
763
+ expect(cacheTrace.cache).toEqual({
764
+ hit: true, sourceTraceId: source.id, sourceTraceTruncated: true,
765
+ });
766
+ expect(cacheTrace.injected.lessons[0]).toMatchObject(lesson('l1', 'memory detail'));
767
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
768
+ });
769
+
770
+ test('replaces one oversized trace with an explicit bounded tombstone', () => {
771
+ const byteBudget = 1200;
772
+ const store = new RetrievalTraceStore({ byteBudget });
773
+ const run = store.begin({
774
+ agentName: TEST_AGENT,
775
+ model: TEST_RETRIEVAL_MODEL,
776
+ minConfidence: 0.3,
777
+ maxCandidates: 20,
778
+ maxInjectedLessons: 5,
779
+ });
780
+ run.setContext('large-context', '🔒'.repeat(20_000), 1, ['message-1']);
781
+ run.finish('no-concepts');
782
+
783
+ const traces = store.list({ limit: 100, includeInputs: true });
784
+ expect(traces).toHaveLength(1);
785
+ expect(traces[0]).toMatchObject({
786
+ outcome: 'no-concepts',
787
+ truncation: {
788
+ truncated: true,
789
+ kind: 'tombstone',
790
+ reason: 'trace-exceeded-byte-budget',
791
+ byteBudget,
792
+ },
793
+ });
794
+ expect(traces[0].context).toBeUndefined();
795
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
796
+ });
797
+
798
+ test('evicted active runs cannot reintroduce payload beyond the byte budget', () => {
799
+ const byteBudget = 2200;
800
+ const store = new RetrievalTraceStore({ byteBudget });
801
+ const begin = () => store.begin({
802
+ agentName: TEST_AGENT,
803
+ model: TEST_RETRIEVAL_MODEL,
804
+ minConfidence: 0.3,
805
+ maxCandidates: 20,
806
+ maxInjectedLessons: 5,
807
+ });
808
+ const older = begin();
809
+ older.setContext('older', 'x'.repeat(1300), 1, ['older-message']);
810
+ const newer = begin();
811
+ newer.setContext('newer', 'y'.repeat(1300), 1, ['newer-message']);
812
+
813
+ expect(store.list({ limit: 100, includeInputs: true }).map(trace => trace.id)).toEqual([2]);
814
+ older.setContext('evicted', 'z'.repeat(100_000), 1, ['evicted-message']);
815
+ older.finish('error', new Error('late active failure'));
816
+ newer.finish('no-concepts');
817
+
818
+ expect(store.list({ limit: 100, includeInputs: true }).map(trace => trace.id)).toEqual([2]);
819
+ expect(store.retainedBytes).toBeLessThanOrEqual(byteBudget);
820
+ });
821
+ });