@animalabs/context-manager 0.5.2 → 0.5.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 (110) hide show
  1. package/dist/src/adaptive/index.d.ts +6 -0
  2. package/dist/src/adaptive/index.d.ts.map +1 -1
  3. package/dist/src/adaptive/index.js +8 -0
  4. package/dist/src/adaptive/index.js.map +1 -1
  5. package/dist/src/adaptive/kv-cache-sim.d.ts +128 -0
  6. package/dist/src/adaptive/kv-cache-sim.d.ts.map +1 -0
  7. package/dist/src/adaptive/kv-cache-sim.js +0 -0
  8. package/dist/src/adaptive/kv-cache-sim.js.map +1 -0
  9. package/dist/src/adaptive/kv-control.d.ts +152 -0
  10. package/dist/src/adaptive/kv-control.d.ts.map +1 -0
  11. package/dist/src/adaptive/kv-control.js +368 -0
  12. package/dist/src/adaptive/kv-control.js.map +1 -0
  13. package/dist/src/adaptive/kv-replay.d.ts +76 -0
  14. package/dist/src/adaptive/kv-replay.d.ts.map +1 -0
  15. package/dist/src/adaptive/kv-replay.js +175 -0
  16. package/dist/src/adaptive/kv-replay.js.map +1 -0
  17. package/dist/src/adaptive/render-offsets.d.ts +61 -0
  18. package/dist/src/adaptive/render-offsets.d.ts.map +1 -0
  19. package/dist/src/adaptive/render-offsets.js +88 -0
  20. package/dist/src/adaptive/render-offsets.js.map +1 -0
  21. package/dist/src/adaptive/strategies/kv-stable.d.ts +48 -0
  22. package/dist/src/adaptive/strategies/kv-stable.d.ts.map +1 -0
  23. package/dist/src/adaptive/strategies/kv-stable.js +102 -0
  24. package/dist/src/adaptive/strategies/kv-stable.js.map +1 -0
  25. package/dist/src/adaptive/summary-tree.d.ts +98 -0
  26. package/dist/src/adaptive/summary-tree.d.ts.map +1 -0
  27. package/dist/src/adaptive/summary-tree.js +214 -0
  28. package/dist/src/adaptive/summary-tree.js.map +1 -0
  29. package/dist/src/context-log.d.ts.map +1 -1
  30. package/dist/src/context-log.js +7 -2
  31. package/dist/src/context-log.js.map +1 -1
  32. package/dist/src/index.d.ts +1 -1
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +1 -1
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/message-store.js +1 -1
  37. package/dist/src/message-store.js.map +1 -1
  38. package/dist/src/normalize-tool-messages.d.ts +33 -0
  39. package/dist/src/normalize-tool-messages.d.ts.map +1 -1
  40. package/dist/src/normalize-tool-messages.js +59 -0
  41. package/dist/src/normalize-tool-messages.js.map +1 -1
  42. package/dist/src/strategies/autobiographical.d.ts +101 -39
  43. package/dist/src/strategies/autobiographical.d.ts.map +1 -1
  44. package/dist/src/strategies/autobiographical.js +398 -222
  45. package/dist/src/strategies/autobiographical.js.map +1 -1
  46. package/dist/src/types/strategy.d.ts +40 -1
  47. package/dist/src/types/strategy.d.ts.map +1 -1
  48. package/dist/src/types/strategy.js +2 -0
  49. package/dist/src/types/strategy.js.map +1 -1
  50. package/dist/test/adaptive/kv-cache-sim.test.d.ts +16 -0
  51. package/dist/test/adaptive/kv-cache-sim.test.d.ts.map +1 -0
  52. package/dist/test/adaptive/kv-cache-sim.test.js +173 -0
  53. package/dist/test/adaptive/kv-cache-sim.test.js.map +1 -0
  54. package/dist/test/adaptive/kv-control.test.d.ts +14 -0
  55. package/dist/test/adaptive/kv-control.test.d.ts.map +1 -0
  56. package/dist/test/adaptive/kv-control.test.js +130 -0
  57. package/dist/test/adaptive/kv-control.test.js.map +1 -0
  58. package/dist/test/adaptive/kv-replay.test.d.ts +15 -0
  59. package/dist/test/adaptive/kv-replay.test.d.ts.map +1 -0
  60. package/dist/test/adaptive/kv-replay.test.js +112 -0
  61. package/dist/test/adaptive/kv-replay.test.js.map +1 -0
  62. package/dist/test/adaptive/kv-stable-strategy.test.d.ts +10 -0
  63. package/dist/test/adaptive/kv-stable-strategy.test.d.ts.map +1 -0
  64. package/dist/test/adaptive/kv-stable-strategy.test.js +89 -0
  65. package/dist/test/adaptive/kv-stable-strategy.test.js.map +1 -0
  66. package/dist/test/adaptive/render-offsets.test.d.ts +11 -0
  67. package/dist/test/adaptive/render-offsets.test.d.ts.map +1 -0
  68. package/dist/test/adaptive/render-offsets.test.js +85 -0
  69. package/dist/test/adaptive/render-offsets.test.js.map +1 -0
  70. package/dist/test/adaptive/summary-tree.test.d.ts +5 -0
  71. package/dist/test/adaptive/summary-tree.test.d.ts.map +1 -0
  72. package/dist/test/adaptive/summary-tree.test.js +72 -0
  73. package/dist/test/adaptive/summary-tree.test.js.map +1 -0
  74. package/dist/test/chunker-tool-boundary.test.d.ts +12 -0
  75. package/dist/test/chunker-tool-boundary.test.d.ts.map +1 -0
  76. package/dist/test/chunker-tool-boundary.test.js +89 -0
  77. package/dist/test/chunker-tool-boundary.test.js.map +1 -0
  78. package/dist/test/compression-shape-invariants.test.d.ts +22 -0
  79. package/dist/test/compression-shape-invariants.test.d.ts.map +1 -0
  80. package/dist/test/compression-shape-invariants.test.js +312 -0
  81. package/dist/test/compression-shape-invariants.test.js.map +1 -0
  82. package/dist/test/image-strip-render-stats.test.d.ts +2 -0
  83. package/dist/test/image-strip-render-stats.test.d.ts.map +1 -0
  84. package/dist/test/image-strip-render-stats.test.js +130 -0
  85. package/dist/test/image-strip-render-stats.test.js.map +1 -0
  86. package/dist/test/image-stripping.test.d.ts +13 -0
  87. package/dist/test/image-stripping.test.d.ts.map +1 -0
  88. package/dist/test/image-stripping.test.js +104 -0
  89. package/dist/test/image-stripping.test.js.map +1 -0
  90. package/dist/test/normalize-tool-messages.test.js +61 -1
  91. package/dist/test/normalize-tool-messages.test.js.map +1 -1
  92. package/dist/test/render-stats.test.d.ts +2 -0
  93. package/dist/test/render-stats.test.d.ts.map +1 -0
  94. package/dist/test/render-stats.test.js +91 -0
  95. package/dist/test/render-stats.test.js.map +1 -0
  96. package/dist/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +2 -2
  98. package/src/adaptive/index.ts +48 -0
  99. package/src/adaptive/kv-cache-sim.ts +0 -0
  100. package/src/adaptive/kv-control.ts +497 -0
  101. package/src/adaptive/kv-replay.ts +238 -0
  102. package/src/adaptive/render-offsets.ts +116 -0
  103. package/src/adaptive/strategies/kv-stable.ts +123 -0
  104. package/src/adaptive/summary-tree.ts +252 -0
  105. package/src/context-log.ts +7 -2
  106. package/src/index.ts +1 -1
  107. package/src/message-store.ts +1 -1
  108. package/src/normalize-tool-messages.ts +61 -0
  109. package/src/strategies/autobiographical.ts +414 -271
  110. package/src/types/strategy.ts +40 -1
@@ -0,0 +1,312 @@
1
+ /**
2
+ * End-to-end shape invariants for the compression pipeline.
3
+ *
4
+ * The whole compression-bug family (5/6/7/8/9) was about the *shape* of the
5
+ * request sent to the Anthropic API, not the content of summaries. This
6
+ * suite exercises the full pipeline (message store → chunker → L1 compress
7
+ * → L2/L3 merges) against a mock membrane that:
8
+ *
9
+ * 1. Validates every request against the API's structural rules
10
+ * (tool_use/tool_result adjacency, placement in user vs. assistant
11
+ * turns) and throws if any rule is violated.
12
+ * 2. Returns a deterministic summary text at a realistic ~5:1 compression
13
+ * ratio so summary IDs and merge cascades behave predictably.
14
+ *
15
+ * If any of the patched call sites — `compressChunkHierarchical`,
16
+ * `executeMerge`, the chunker's tool-cycle-respecting close, or the
17
+ * runtime `splitMixedToolMessages` / `stripUnpairedToolBlocks` passes —
18
+ * regresses, the mock will throw with the same error shape the live API
19
+ * would return, and the test fails.
20
+ */
21
+ import { describe, it, before, after } from 'node:test';
22
+ import assert from 'node:assert';
23
+ import { rmSync, existsSync } from 'node:fs';
24
+ import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
25
+ const TEST_STORE_PATH = './test-compression-shape';
26
+ function cleanup() {
27
+ if (existsSync(TEST_STORE_PATH)) {
28
+ rmSync(TEST_STORE_PATH, { recursive: true, force: true });
29
+ }
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // API-shape validator: mirrors what the Anthropic API enforces.
33
+ // ---------------------------------------------------------------------------
34
+ function validateApiShape(messages) {
35
+ for (let i = 0; i < messages.length; i++) {
36
+ const msg = messages[i];
37
+ const isUser = msg.participant.toLowerCase() === 'user';
38
+ for (const block of msg.content) {
39
+ if (block.type === 'tool_result' && !isUser) {
40
+ throw new Error(`messages[${i}]: tool_result block in non-user turn (participant=${msg.participant})`);
41
+ }
42
+ if (block.type === 'tool_use') {
43
+ const id = block.id;
44
+ const next = messages[i + 1];
45
+ const matched = next?.content.some((b) => b.type === 'tool_result' &&
46
+ b.toolUseId === id);
47
+ if (!matched) {
48
+ throw new Error(`messages[${i}]: tool_use id=${id} was not followed by a matching tool_result in messages[${i + 1}]`);
49
+ }
50
+ }
51
+ if (block.type === 'tool_result') {
52
+ const tid = block.toolUseId;
53
+ const prev = messages[i - 1];
54
+ const matched = prev?.content.some((b) => b.type === 'tool_use' && b.id === tid);
55
+ if (!matched) {
56
+ throw new Error(`messages[${i}]: tool_result toolUseId=${tid} was not preceded by a matching tool_use in messages[${i - 1}]`);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Mock membrane: validates shape, returns deterministic compressed text.
64
+ // ---------------------------------------------------------------------------
65
+ function createValidatingMembrane(compressionRatio = 5) {
66
+ const calls = [];
67
+ const membrane = {
68
+ complete: async (request) => {
69
+ validateApiShape(request.messages);
70
+ const inputChars = request.messages
71
+ .flatMap((m) => m.content)
72
+ .map((b) => b.text ?? '')
73
+ .join('').length;
74
+ const targetChars = Math.max(60, Math.floor(inputChars / compressionRatio));
75
+ const summary = `[mock summary inChars=${inputChars}] ` +
76
+ 'x '.repeat(Math.max(0, Math.floor((targetChars - 30) / 2)));
77
+ calls.push({ messages: request.messages, outputChars: summary.length });
78
+ return {
79
+ content: [{ type: 'text', text: summary }],
80
+ usage: {
81
+ input_tokens: Math.ceil(inputChars / 4),
82
+ output_tokens: Math.ceil(summary.length / 4),
83
+ },
84
+ };
85
+ },
86
+ };
87
+ return { membrane, calls };
88
+ }
89
+ const t = (s) => ({ type: 'text', text: s });
90
+ const u = (id) => ({ type: 'tool_use', id, name: 'fn', input: {} });
91
+ const r = (id) => ({ type: 'tool_result', toolUseId: id, content: 'ok' });
92
+ async function drain(manager) {
93
+ // Cap iterations so a regression that fails to converge can't hang CI.
94
+ for (let i = 0; i < 500; i++) {
95
+ if (manager.isReady())
96
+ return;
97
+ await manager.tick();
98
+ }
99
+ throw new Error('drain: queue did not converge within 500 ticks');
100
+ }
101
+ describe('Compression pipeline: API shape invariants', () => {
102
+ before(() => cleanup());
103
+ after(() => cleanup());
104
+ it('L1 compression: chunker close lands on a tool_use without fix — must not throw', async () => {
105
+ cleanup();
106
+ const { membrane, calls } = createValidatingMembrane();
107
+ const strategy = new AutobiographicalStrategy({
108
+ targetChunkTokens: 80,
109
+ headWindowTokens: 0,
110
+ recentWindowTokens: 0,
111
+ hierarchical: true,
112
+ });
113
+ const manager = await ContextManager.open({
114
+ path: TEST_STORE_PATH,
115
+ strategy,
116
+ membrane: membrane,
117
+ });
118
+ // Force the chunker to want to close on a tool_use-bearing message:
119
+ // - 3 small messages (length 3, tokens ~30)
120
+ // - then a FAT agent message with a tool_use (length 4 → meets >=4,
121
+ // fat enough to push currentTokens past targetChunkTokens=80)
122
+ // - then the matching tool_result (would land in next chunk
123
+ // without the chunker-defer fix → orphan tool_use at boundary)
124
+ // Repeat this pattern many times so multiple boundaries are exercised.
125
+ const small = (n) => 'word '.repeat(n);
126
+ const fat = (n) => 'thinking '.repeat(n);
127
+ for (let i = 0; i < 12; i++) {
128
+ manager.addMessage('user', [t(small(8))]);
129
+ manager.addMessage('agent', [t(small(8))]);
130
+ manager.addMessage('user', [t(small(8))]);
131
+ manager.addMessage('agent', [t(fat(60)), u(`A${i}`)]);
132
+ manager.addMessage('user', [r(`A${i}`)]);
133
+ manager.addMessage('agent', [t(small(10))]);
134
+ }
135
+ await drain(manager);
136
+ assert.ok(calls.length > 0, 'expected at least one L1 compression call');
137
+ await manager.close();
138
+ });
139
+ it('L2/L3 merge cascade preserves shape across hundreds of summaries', async () => {
140
+ cleanup();
141
+ const { membrane, calls } = createValidatingMembrane();
142
+ const strategy = new AutobiographicalStrategy({
143
+ targetChunkTokens: 50,
144
+ headWindowTokens: 0,
145
+ recentWindowTokens: 0,
146
+ hierarchical: true,
147
+ mergeThreshold: 3, // L1→L2 every 3 L1s, L2→L3 every 9 L1s
148
+ });
149
+ const manager = await ContextManager.open({
150
+ path: TEST_STORE_PATH,
151
+ strategy,
152
+ membrane: membrane,
153
+ });
154
+ const filler = (n) => 'word '.repeat(n);
155
+ for (let i = 0; i < 80; i++) {
156
+ if (i % 5 === 0 && i > 0) {
157
+ manager.addMessage('agent', [t(filler(10)), u(`B${i}`)]);
158
+ manager.addMessage('user', [r(`B${i}`)]);
159
+ }
160
+ else {
161
+ manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [t(filler(15))]);
162
+ }
163
+ }
164
+ await drain(manager);
165
+ // We should have hit L1 + at least one L2 merge — easiest signal is the
166
+ // sheer call count plus the absence of validation throws.
167
+ const snap = strategy.getProgressSnapshot();
168
+ assert.ok(snap.summaryCounts.l1 > 0, 'expected L1 summaries');
169
+ assert.ok(snap.summaryCounts.l2 > 0, `expected at least one L2 merge (l1=${snap.summaryCounts.l1}, l2=${snap.summaryCounts.l2})`);
170
+ assert.ok(calls.length >= snap.summaryCounts.l1 + snap.summaryCounts.l2);
171
+ await manager.close();
172
+ });
173
+ it('Bug 10: raw middle dedup correctly handles L2+ summaries (no message leak)', async () => {
174
+ cleanup();
175
+ const { membrane, calls } = createValidatingMembrane();
176
+ const strategy = new AutobiographicalStrategy({
177
+ targetChunkTokens: 40,
178
+ headWindowTokens: 0,
179
+ recentWindowTokens: 0,
180
+ hierarchical: true,
181
+ mergeThreshold: 3,
182
+ });
183
+ const manager = await ContextManager.open({
184
+ path: TEST_STORE_PATH,
185
+ strategy,
186
+ membrane: membrane,
187
+ });
188
+ // 100 messages → enough chunks to drive a multi-level merge cascade
189
+ // (L1s → L2s → L3) so executeMerge runs with L2s in its prior
190
+ // frontier. Each message has a distinctive marker (`RAW-N`) so we
191
+ // can count how many leak into requests as raw content.
192
+ const filler = (n) => 'word '.repeat(n);
193
+ for (let i = 0; i < 100; i++) {
194
+ manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [
195
+ t(`RAW-${i} ${filler(8)}`),
196
+ ]);
197
+ }
198
+ await drain(manager);
199
+ const snap = strategy.getProgressSnapshot();
200
+ assert.ok(snap.summaryCounts.l2 >= 2, `expected >=2 L2 summaries to exercise the bug (got l2=${snap.summaryCounts.l2})`);
201
+ // Count raw conversation messages in each request. The expansion bug
202
+ // allowed ~all messages to leak in (the bug report saw 256 user +
203
+ // 262 agent for a 4234-msg conversation). With the fix, per-request
204
+ // raw-message count should be bounded by the merge target's leaf
205
+ // size — for mergeThreshold=3 with ~3-msg L1s, that's <= ~25.
206
+ const RAW_MSG_LIMIT = 40;
207
+ for (let i = 0; i < calls.length; i++) {
208
+ const call = calls[i];
209
+ let rawCount = 0;
210
+ for (const m of call.messages) {
211
+ for (const block of m.content) {
212
+ const text = block.text ?? '';
213
+ if (text.includes('RAW-'))
214
+ rawCount++;
215
+ }
216
+ }
217
+ assert.ok(rawCount <= RAW_MSG_LIMIT, `call ${i}: ${rawCount} raw messages in request — raw middle is leaking already-summarized content (Bug 10)`);
218
+ }
219
+ await manager.close();
220
+ });
221
+ it('runtime splitMixedToolMessages handles bundled cycles in source data', async () => {
222
+ cleanup();
223
+ const { membrane, calls } = createValidatingMembrane();
224
+ const strategy = new AutobiographicalStrategy({
225
+ targetChunkTokens: 60,
226
+ headWindowTokens: 0,
227
+ recentWindowTokens: 0,
228
+ hierarchical: true,
229
+ });
230
+ const manager = await ContextManager.open({
231
+ path: TEST_STORE_PATH,
232
+ strategy,
233
+ membrane: membrane,
234
+ });
235
+ // Pre-Bug-8 import shape: tool_use AND tool_result bundled into one
236
+ // assistant message. The runtime split has to unpack these before the
237
+ // request reaches the membrane.
238
+ for (let i = 0; i < 20; i++) {
239
+ manager.addMessage('user', [t('q'.repeat(60))]);
240
+ manager.addMessage('agent', [
241
+ t('a'.repeat(40)),
242
+ u(`C${i}`),
243
+ r(`C${i}`),
244
+ t('done'),
245
+ ]);
246
+ }
247
+ await drain(manager);
248
+ assert.ok(calls.length > 0);
249
+ await manager.close();
250
+ });
251
+ it('orphan tool_use at the very tail (no next message ever) does not crash', async () => {
252
+ cleanup();
253
+ const { membrane, calls } = createValidatingMembrane();
254
+ const strategy = new AutobiographicalStrategy({
255
+ targetChunkTokens: 50,
256
+ headWindowTokens: 0,
257
+ recentWindowTokens: 0,
258
+ hierarchical: true,
259
+ });
260
+ const manager = await ContextManager.open({
261
+ path: TEST_STORE_PATH,
262
+ strategy,
263
+ membrane: membrane,
264
+ });
265
+ // Build a sequence that ends on a tool_use with no following tool_result.
266
+ // The chunker's defer rule can't help here (there's no next message);
267
+ // stripUnpairedToolBlocks is the safety net.
268
+ const filler = (n) => 'word '.repeat(n);
269
+ for (let i = 0; i < 12; i++) {
270
+ manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [t(filler(20))]);
271
+ }
272
+ manager.addMessage('agent', [t(filler(10)), u('orphan')]);
273
+ await drain(manager);
274
+ assert.ok(calls.length > 0);
275
+ await manager.close();
276
+ });
277
+ it('adaptive resolution variant uses the same compression sites — same shape guarantees', async () => {
278
+ cleanup();
279
+ const { membrane, calls } = createValidatingMembrane();
280
+ const strategy = new AutobiographicalStrategy({
281
+ targetChunkTokens: 50,
282
+ headWindowTokens: 0,
283
+ recentWindowTokens: 0,
284
+ hierarchical: true,
285
+ adaptiveResolution: true,
286
+ });
287
+ const manager = await ContextManager.open({
288
+ path: TEST_STORE_PATH,
289
+ strategy,
290
+ membrane: membrane,
291
+ });
292
+ const filler = (n) => 'word '.repeat(n);
293
+ for (let i = 0; i < 30; i++) {
294
+ if (i % 4 === 0 && i > 0) {
295
+ manager.addMessage('agent', [t(filler(10)), u(`D${i}`)]);
296
+ manager.addMessage('user', [r(`D${i}`)]);
297
+ }
298
+ else {
299
+ manager.addMessage(i % 2 === 0 ? 'agent' : 'user', [t(filler(20))]);
300
+ }
301
+ }
302
+ // Adaptive path produces L1s lazily via the picker — trigger a compile
303
+ // (or two) so FoldOps run and enqueue compression work.
304
+ await manager.compile();
305
+ await drain(manager);
306
+ await manager.compile();
307
+ await drain(manager);
308
+ assert.ok(calls.length > 0, 'adaptive path produced no compression calls');
309
+ await manager.close();
310
+ });
311
+ });
312
+ //# sourceMappingURL=compression-shape-invariants.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compression-shape-invariants.test.js","sourceRoot":"","sources":["../../test/compression-shape-invariants.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAG3E,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAEnD,SAAS,OAAO;IACd,IAAI,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QAChC,MAAM,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAID,8EAA8E;AAC9E,gEAAgE;AAChE,8EAA8E;AAE9E,SAAS,gBAAgB,CAAC,QAA+B;IACvD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;QAExD,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YAChC,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC5C,MAAM,IAAI,KAAK,CACb,YAAY,CAAC,sDAAsD,GAAG,CAAC,WAAW,GAAG,CACtF,CAAC;YACJ,CAAC;YAED,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC9B,MAAM,EAAE,GAAI,KAAwB,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC7B,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,IAAI,KAAK,aAAa;oBACvB,CAA2B,CAAC,SAAS,KAAK,EAAE,CAChD,CAAC;gBACF,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,MAAM,IAAI,KAAK,CACb,YAAY,CAAC,kBAAkB,EAAE,2DAA2D,CAAC,GAAG,CAAC,GAAG,CACrG,CAAC;gBACJ,CAAC;YACH,CAAC;YAED,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;gBACjC,MAAM,GAAG,GAAI,KAA+B,CAAC,SAAS,CAAC;gBACvD,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC7B,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAK,CAAoB,CAAC,EAAE,KAAK,GAAG,CACjE,CAAC;gBACF,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,MAAM,IAAI,KAAK,CACb,YAAY,CAAC,4BAA4B,GAAG,wDAAwD,CAAC,GAAG,CAAC,GAAG,CAC7G,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,yEAAyE;AACzE,8EAA8E;AAE9E,SAAS,wBAAwB,CAAC,gBAAgB,GAAG,CAAC;IACpD,MAAM,KAAK,GAA2D,EAAE,CAAC;IACzE,MAAM,QAAQ,GAAG;QACf,QAAQ,EAAE,KAAK,EAAE,OAGhB,EAAE,EAAE;YACH,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACnC,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ;iBAChC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;iBACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAuB,CAAC,IAAI,IAAI,EAAE,CAAC;iBAC/C,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;YACnB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAC,CAAC,CAAC;YAC5E,MAAM,OAAO,GACX,yBAAyB,UAAU,IAAI;gBACvC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/D,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YACxE,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;gBAC1C,KAAK,EAAE;oBACL,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;oBACvC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;iBAC7C;aACF,CAAC;QACJ,CAAC;KACF,CAAC;IACF,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,CAAC,GAAG,CAAC,CAAS,EAAgB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;AACnE,MAAM,CAAC,GAAG,CAAC,EAAU,EAAgB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;AAC1F,MAAM,CAAC,GAAG,CAAC,EAAU,EAAgB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AAEhG,KAAK,UAAU,KAAK,CAAC,OAAuB;IAC1C,uEAAuE;IACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,OAAO,EAAE;YAAE,OAAO;QAC9B,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;AACpE,CAAC;AAED,QAAQ,CAAC,4CAA4C,EAAE,GAAG,EAAE;IAC1D,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IAEvB,EAAE,CAAC,gFAAgF,EAAE,KAAK,IAAI,EAAE;QAC9F,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,wBAAwB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAI,wBAAwB,CAAC;YAC5C,iBAAiB,EAAE,EAAE;YACrB,gBAAgB,EAAE,CAAC;YACnB,kBAAkB,EAAE,CAAC;YACrB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ;YACR,QAAQ,EAAE,QAAe;SAC1B,CAAC,CAAC;QAEH,oEAAoE;QACpE,8CAA8C;QAC9C,sEAAsE;QACtE,kEAAkE;QAClE,8DAA8D;QAC9D,mEAAmE;QACnE,uEAAuE;QACvE,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3C,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACtD,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACzC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,CAAC;QAED,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,2CAA2C,CAAC,CAAC;QACzE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAChF,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,wBAAwB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAI,wBAAwB,CAAC;YAC5C,iBAAiB,EAAE,EAAE;YACrB,gBAAgB,EAAE,CAAC;YACnB,kBAAkB,EAAE,CAAC;YACrB,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,CAAC,EAAE,uCAAuC;SAC3D,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ;YACR,QAAQ,EAAE,QAAe;SAC1B,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBACzD,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACtE,CAAC;QACH,CAAC;QAED,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QAErB,wEAAwE;QACxE,0DAA0D;QAC1D,MAAM,IAAI,GAAG,QAAQ,CAAC,mBAAmB,EAAE,CAAC;QAC5C,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,GAAG,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAC9D,MAAM,CAAC,EAAE,CACP,IAAI,CAAC,aAAa,CAAC,EAAE,GAAG,CAAC,EACzB,sCAAsC,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,IAAI,CAAC,aAAa,CAAC,EAAE,GAAG,CAC5F,CAAC;QACF,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,aAAa,CAAC,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QACzE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,wBAAwB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAI,wBAAwB,CAAC;YAC5C,iBAAiB,EAAE,EAAE;YACrB,gBAAgB,EAAE,CAAC;YACnB,kBAAkB,EAAE,CAAC;YACrB,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,CAAC;SAClB,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ;YACR,QAAQ,EAAE,QAAe;SAC1B,CAAC,CAAC;QAEH,oEAAoE;QACpE,8DAA8D;QAC9D,kEAAkE;QAClE,wDAAwD;QACxD,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7B,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE;gBACjD,CAAC,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3B,CAAC,CAAC;QACL,CAAC;QAED,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QAErB,MAAM,IAAI,GAAG,QAAQ,CAAC,mBAAmB,EAAE,CAAC;QAC5C,MAAM,CAAC,EAAE,CACP,IAAI,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,EAC1B,yDAAyD,IAAI,CAAC,aAAa,CAAC,EAAE,GAAG,CAClF,CAAC;QAEF,qEAAqE;QACrE,kEAAkE;QAClE,oEAAoE;QACpE,iEAAiE;QACjE,8DAA8D;QAC9D,MAAM,aAAa,GAAG,EAAE,CAAC;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,IAAI,QAAQ,GAAG,CAAC,CAAC;YACjB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC9B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;oBAC9B,MAAM,IAAI,GAAI,KAA2B,CAAC,IAAI,IAAI,EAAE,CAAC;oBACrD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBAAE,QAAQ,EAAE,CAAC;gBACxC,CAAC;YACH,CAAC;YACD,MAAM,CAAC,EAAE,CACP,QAAQ,IAAI,aAAa,EACzB,QAAQ,CAAC,KAAK,QAAQ,sFAAsF,CAC7G,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,wBAAwB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAI,wBAAwB,CAAC;YAC5C,iBAAiB,EAAE,EAAE;YACrB,gBAAgB,EAAE,CAAC;YACnB,kBAAkB,EAAE,CAAC;YACrB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ;YACR,QAAQ,EAAE,QAAe;SAC1B,CAAC,CAAC;QAEH,oEAAoE;QACpE,sEAAsE;QACtE,gCAAgC;QAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE;gBAC1B,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACjB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACV,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACV,CAAC,CAAC,MAAM,CAAC;aACV,CAAC,CAAC;QACL,CAAC;QAED,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5B,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;QACtF,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,wBAAwB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAI,wBAAwB,CAAC;YAC5C,iBAAiB,EAAE,EAAE;YACrB,gBAAgB,EAAE,CAAC;YACnB,kBAAkB,EAAE,CAAC;YACrB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ;YACR,QAAQ,EAAE,QAAe;SAC1B,CAAC,CAAC;QAEH,0EAA0E;QAC1E,sEAAsE;QACtE,6CAA6C;QAC7C,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACtE,CAAC;QACD,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAE1D,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5B,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qFAAqF,EAAE,KAAK,IAAI,EAAE;QACnG,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,wBAAwB,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAI,wBAAwB,CAAC;YAC5C,iBAAiB,EAAE,EAAE;YACrB,gBAAgB,EAAE,CAAC;YACnB,kBAAkB,EAAE,CAAC;YACrB,YAAY,EAAE,IAAI;YAClB,kBAAkB,EAAE,IAAI;SACzB,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ;YACR,QAAQ,EAAE,QAAe;SAC1B,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBACzD,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACtE,CAAC;QACH,CAAC;QAED,uEAAuE;QACvE,wDAAwD;QACxD,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QAErB,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,6CAA6C,CAAC,CAAC;QAC3E,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=image-strip-render-stats.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-strip-render-stats.test.d.ts","sourceRoot":"","sources":["../../test/image-strip-render-stats.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Regression: image stripping must run BEFORE the render-stats snapshot is
3
+ * committed and BEFORE cache markers are placed, so that
4
+ *
5
+ * (a) RenderStats.total reflects the POST-strip context — the real bytes the
6
+ * agent receives — not the pre-strip size that still counts full image
7
+ * weight (≈1600 tok) for images replaced by a short text placeholder.
8
+ * Pre-fix, getRenderStats() over-reported by ~1592 tok per stripped image
9
+ * (and image stripping is on by default: maxLiveImages = 6).
10
+ *
11
+ * (b) cache markers and stripping coexist cleanly: marker positions are
12
+ * structural (content-independent, so deterministic across recompiles)
13
+ * and a stripped image never resurrects across turns — the live-image
14
+ * window is monotonic, so each image strips at most once (one bounded KV
15
+ * perturbation, not per-turn flicker).
16
+ */
17
+ import { describe, it, before, after, beforeEach } from 'node:test';
18
+ import assert from 'node:assert';
19
+ import { rmSync, existsSync } from 'node:fs';
20
+ import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
21
+ const TEST_STORE_PATH = './test-image-strip-render-stats';
22
+ const PLACEHOLDER = 'image dropped from live context';
23
+ function cleanup() {
24
+ if (existsSync(TEST_STORE_PATH))
25
+ rmSync(TEST_STORE_PATH, { recursive: true, force: true });
26
+ }
27
+ /** A message carrying one image plus a text label so we can locate it later. */
28
+ function imageMessage(label, tokenEstimate = 1600) {
29
+ return [
30
+ { type: 'image', source: { type: 'base64', data: 'AAAA', mediaType: 'image/png' }, tokenEstimate },
31
+ { type: 'text', text: label },
32
+ ];
33
+ }
34
+ /** Re-derive rendered tokens from the COMPILED output using the strategy's
35
+ * default estimator (image -> tokenEstimate ?? 1600, text -> ceil(len/4)).
36
+ * The fixtures use only image + text blocks. */
37
+ function renderedTokens(messages) {
38
+ let t = 0;
39
+ for (const m of messages) {
40
+ for (const b of m.content) {
41
+ if (b.type === 'image')
42
+ t += b.tokenEstimate ?? 1600;
43
+ else if (b.type === 'text')
44
+ t += Math.ceil(b.text.length / 4);
45
+ }
46
+ }
47
+ return t;
48
+ }
49
+ function hasLabel(content, label) {
50
+ return content.some((b) => b.type === 'text' && b.text.includes(label));
51
+ }
52
+ describe('image stripping × render stats', () => {
53
+ before(cleanup);
54
+ after(cleanup);
55
+ beforeEach(cleanup);
56
+ it('(a) total reflects the post-strip size, not the pre-strip image weight', async () => {
57
+ // Default live-image policy is ON (maxLiveImages: 6). 10 images => 4 stripped.
58
+ const manager = await ContextManager.open({
59
+ path: TEST_STORE_PATH,
60
+ strategy: new AutobiographicalStrategy({
61
+ adaptiveResolution: true,
62
+ headWindowTokens: 0,
63
+ recentWindowTokens: 1_000_000, // keep everything verbatim; isolate the count cap
64
+ }),
65
+ });
66
+ for (let i = 1; i <= 10; i++)
67
+ manager.addMessage('user', imageMessage(`img-${String(i).padStart(2, '0')}`));
68
+ const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
69
+ const live = compiled.messages.filter((m) => m.content.some((b) => b.type === 'image')).length;
70
+ const stripped = compiled.messages.filter((m) => hasLabel(m.content, PLACEHOLDER)).length;
71
+ assert.equal(live, 6, 'default maxLiveImages keeps 6 images live');
72
+ assert.equal(stripped, 4, 'the other 4 are stripped to placeholders');
73
+ const rs = manager.getRenderStats();
74
+ assert.ok(rs, 'stats present after compile');
75
+ // Decisive assertion: total == the ACTUAL rendered size. Pre-fix this was
76
+ // inflated by the 4 stripped images (~4 * 1592 tok).
77
+ const expected = renderedTokens(compiled.messages);
78
+ assert.equal(rs.total.tokens, expected, 'total.tokens equals the real rendered size');
79
+ // Internal invariant must still hold (total == sum of buckets).
80
+ const s = rs.summaries;
81
+ const bucketSum = rs.head.tokens + rs.tail.tokens + rs.middleRaw.tokens +
82
+ s.l1.tokens + s.l2.tokens + s.l3.tokens;
83
+ assert.equal(rs.total.tokens, bucketSum, 'total == sum of buckets');
84
+ for (const k of ['head', 'tail', 'middleRaw']) {
85
+ assert.ok(rs[k].tokens >= 0, `${k} tokens stay non-negative after the strip credit`);
86
+ }
87
+ // And total must be strictly below the pre-strip size — proves the credit bites.
88
+ const preStrip = expected + stripped * (1600 - Math.ceil('[image dropped from live context]'.length / 4));
89
+ assert.ok(rs.total.tokens < preStrip, 'total is the stripped size, not the pre-strip size');
90
+ manager.close();
91
+ });
92
+ it('(b) cache markers coexist with stripping; stripped images never resurrect', async () => {
93
+ const manager = await ContextManager.open({
94
+ path: TEST_STORE_PATH,
95
+ strategy: new AutobiographicalStrategy({
96
+ adaptiveResolution: true,
97
+ headWindowTokens: 0,
98
+ recentWindowTokens: 1_000_000,
99
+ maxLiveImages: 3,
100
+ imageStripDepthTokens: 0, // isolate the count cap
101
+ }),
102
+ });
103
+ for (let i = 1; i <= 6; i++)
104
+ manager.addMessage('user', imageMessage(`img-${String(i).padStart(2, '0')}`));
105
+ const c1 = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
106
+ const markers1 = c1.messages.flatMap((m, i) => (m.cacheBreakpoint ? [i] : []));
107
+ const stripped1 = c1.messages.filter((m) => hasLabel(m.content, PLACEHOLDER)).length;
108
+ assert.ok(markers1.length > 0, 'cache markers are placed');
109
+ assert.equal(stripped1, 3, 'oldest 3 of 6 images stripped under maxLiveImages:3');
110
+ // Re-compile the SAME store: marker placement is structural (head/tail
111
+ // membership + index), so positions are identical even though stripping
112
+ // mutated content. Guards against a future token-sensitive marker pass
113
+ // being fed pre-strip sizes.
114
+ const c1b = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
115
+ const markers1b = c1b.messages.flatMap((m, i) => (m.cacheBreakpoint ? [i] : []));
116
+ assert.deepEqual(markers1b, markers1, 'marker positions are deterministic across recompiles');
117
+ // Cross-turn: append a new image so the live window slides forward. Images
118
+ // that were already stripped must STAY stripped — monotonic, no flicker
119
+ // that would churn the cached prefix turn-to-turn.
120
+ manager.addMessage('user', imageMessage('img-07'));
121
+ const c2 = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
122
+ for (const label of ['img-01', 'img-02', 'img-03']) {
123
+ const e = c2.messages.find((m) => hasLabel(m.content, label));
124
+ assert.ok(e, `${label} still present`);
125
+ assert.ok(!e.content.some((b) => b.type === 'image'), `${label} stays stripped after a new turn`);
126
+ }
127
+ manager.close();
128
+ });
129
+ });
130
+ //# sourceMappingURL=image-strip-render-stats.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-strip-render-stats.test.js","sourceRoot":"","sources":["../../test/image-strip-render-stats.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAG3E,MAAM,eAAe,GAAG,iCAAiC,CAAC;AAC1D,MAAM,WAAW,GAAG,iCAAiC,CAAC;AAEtD,SAAS,OAAO;IACd,IAAI,UAAU,CAAC,eAAe,CAAC;QAAE,MAAM,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7F,CAAC;AAED,gFAAgF;AAChF,SAAS,YAAY,CAAC,KAAa,EAAE,aAAa,GAAG,IAAI;IACvD,OAAO;QACL,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,aAAa,EAAkB;QAClH,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE;KAC9B,CAAC;AACJ,CAAC;AAED;;iDAEiD;AACjD,SAAS,cAAc,CAAC,QAAuC;IAC7D,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO;gBAAE,CAAC,IAAK,CAAgC,CAAC,aAAa,IAAI,IAAI,CAAC;iBAChF,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;gBAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,QAAQ,CAAC,OAAuB,EAAE,KAAa;IACtD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,MAAM,CAAC,OAAO,CAAC,CAAC;IAChB,KAAK,CAAC,OAAO,CAAC,CAAC;IACf,UAAU,CAAC,OAAO,CAAC,CAAC;IAEpB,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;QACtF,+EAA+E;QAC/E,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ,EAAE,IAAI,wBAAwB,CAAC;gBACrC,kBAAkB,EAAE,IAAI;gBACxB,gBAAgB,EAAE,CAAC;gBACnB,kBAAkB,EAAE,SAAS,EAAE,kDAAkD;aAClF,CAAC;SACH,CAAC,CAAC;QACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE;YAAE,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAC5G,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC,CAAC;QAExF,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/F,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;QAC1F,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,2CAA2C,CAAC,CAAC;QACnE,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,EAAE,0CAA0C,CAAC,CAAC;QAEtE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;QACpC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,6BAA6B,CAAC,CAAC;QAE7C,0EAA0E;QAC1E,qDAAqD;QACrD,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,EAAG,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,4CAA4C,CAAC,CAAC;QAEvF,gEAAgE;QAChE,MAAM,CAAC,GAAG,EAAG,CAAC,SAAS,CAAC;QACxB,MAAM,SAAS,GACb,EAAG,CAAC,IAAI,CAAC,MAAM,GAAG,EAAG,CAAC,IAAI,CAAC,MAAM,GAAG,EAAG,CAAC,SAAS,CAAC,MAAM;YACxD,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;QAC1C,MAAM,CAAC,KAAK,CAAC,EAAG,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,yBAAyB,CAAC,CAAC;QACrE,KAAK,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAU,EAAE,CAAC;YACvD,MAAM,CAAC,EAAE,CAAC,EAAG,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,EAAE,GAAG,CAAC,kDAAkD,CAAC,CAAC;QACxF,CAAC;QAED,iFAAiF;QACjF,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,mCAAmC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1G,MAAM,CAAC,EAAE,CAAC,EAAG,CAAC,KAAK,CAAC,MAAM,GAAG,QAAQ,EAAE,oDAAoD,CAAC,CAAC;QAE7F,OAAO,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;QACzF,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;YACxC,IAAI,EAAE,eAAe;YACrB,QAAQ,EAAE,IAAI,wBAAwB,CAAC;gBACrC,kBAAkB,EAAE,IAAI;gBACxB,gBAAgB,EAAE,CAAC;gBACnB,kBAAkB,EAAE,SAAS;gBAC7B,aAAa,EAAE,CAAC;gBAChB,qBAAqB,EAAE,CAAC,EAAE,wBAAwB;aACnD,CAAC;SACH,CAAC,CAAC;QACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;YAAE,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAE3G,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC,CAAC;QAClF,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAE,CAAmC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAClH,MAAM,SAAS,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;QAErF,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,0BAA0B,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,EAAE,qDAAqD,CAAC,CAAC;QAElF,uEAAuE;QACvE,wEAAwE;QACxE,uEAAuE;QACvE,6BAA6B;QAC7B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC,CAAC;QACnF,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAE,CAAmC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpH,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,QAAQ,EAAE,sDAAsD,CAAC,CAAC;QAE9F,2EAA2E;QAC3E,wEAAwE;QACxE,mDAAmD;QACnD,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;QACnD,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC,CAAC;QAClF,KAAK,MAAM,KAAK,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;YAC9D,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,gBAAgB,CAAC,CAAC;YACvC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,EAAE,GAAG,KAAK,kCAAkC,CAAC,CAAC;QACrG,CAAC;QAED,OAAO,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Tests for the live-image policy in AutobiographicalStrategy:
3
+ *
4
+ * (1) maxLiveImages — a hard count ceiling. Only the N most-recent images
5
+ * survive as real image blocks; older ones become text placeholders.
6
+ * (2) imageStripDepthTokens — a depth cutoff. Images deeper than D tokens
7
+ * from the newest message are stripped even though the surrounding text
8
+ * stays verbatim (so text horizon >> image horizon).
9
+ *
10
+ * Stripped images are replaced by "[image dropped from live context]".
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=image-stripping.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-stripping.test.d.ts","sourceRoot":"","sources":["../../test/image-stripping.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Tests for the live-image policy in AutobiographicalStrategy:
3
+ *
4
+ * (1) maxLiveImages — a hard count ceiling. Only the N most-recent images
5
+ * survive as real image blocks; older ones become text placeholders.
6
+ * (2) imageStripDepthTokens — a depth cutoff. Images deeper than D tokens
7
+ * from the newest message are stripped even though the surrounding text
8
+ * stays verbatim (so text horizon >> image horizon).
9
+ *
10
+ * Stripped images are replaced by "[image dropped from live context]".
11
+ */
12
+ import { describe, it, before, after } from 'node:test';
13
+ import assert from 'node:assert';
14
+ import { rmSync, existsSync } from 'node:fs';
15
+ import { ContextManager, AutobiographicalStrategy } from '../src/index.js';
16
+ const TEST_STORE_PATH = './test-image-stripping';
17
+ function cleanup() {
18
+ if (existsSync(TEST_STORE_PATH))
19
+ rmSync(TEST_STORE_PATH, { recursive: true, force: true });
20
+ }
21
+ /** A message carrying one image plus a text label so we can locate it later. */
22
+ function imageMessage(label, tokenEstimate = 1600) {
23
+ return [
24
+ { type: 'image', source: { type: 'base64', data: 'AAAA', mediaType: 'image/png' }, tokenEstimate },
25
+ { type: 'text', text: label },
26
+ ];
27
+ }
28
+ /** Find the compiled entry whose text contains `label`, and report whether it
29
+ * still holds a real image block or was reduced to the placeholder. */
30
+ function imageState(messages, label) {
31
+ const entry = messages.find((m) => m.content.some((b) => b.type === 'text' && b.text.includes(label)));
32
+ if (!entry)
33
+ return 'missing';
34
+ if (entry.content.some((b) => b.type === 'image'))
35
+ return 'image';
36
+ if (entry.content.some((b) => b.type === 'text' && b.text.includes('image dropped from live context')))
37
+ return 'stripped';
38
+ return 'missing';
39
+ }
40
+ describe('Live image policy', () => {
41
+ before(() => cleanup());
42
+ after(() => cleanup());
43
+ it('keeps only the maxLiveImages most-recent images (count cap)', async () => {
44
+ cleanup();
45
+ const strategy = new AutobiographicalStrategy({
46
+ headWindowTokens: 0,
47
+ recentWindowTokens: 1_000_000, // keep everything verbatim
48
+ hierarchical: true,
49
+ maxLiveImages: 2,
50
+ imageStripDepthTokens: 0, // disable depth strip — isolate the count cap
51
+ });
52
+ const manager = await ContextManager.open({ path: TEST_STORE_PATH, strategy });
53
+ for (let i = 1; i <= 4; i++)
54
+ manager.addMessage('user', imageMessage(`img-0${i}`));
55
+ const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
56
+ // Newest two (img-03, img-04) keep their image; older two are stripped.
57
+ assert.equal(imageState(compiled.messages, 'img-04'), 'image', 'newest image kept');
58
+ assert.equal(imageState(compiled.messages, 'img-03'), 'image', '2nd-newest image kept');
59
+ assert.equal(imageState(compiled.messages, 'img-02'), 'stripped', '3rd-newest stripped by count cap');
60
+ assert.equal(imageState(compiled.messages, 'img-01'), 'stripped', 'oldest stripped by count cap');
61
+ manager.close();
62
+ });
63
+ it('strips images deeper than imageStripDepthTokens (depth cutoff)', async () => {
64
+ cleanup();
65
+ const strategy = new AutobiographicalStrategy({
66
+ headWindowTokens: 0,
67
+ recentWindowTokens: 1_000_000, // text horizon stays huge
68
+ hierarchical: true,
69
+ maxLiveImages: 0, // unlimited count — isolate the depth cutoff
70
+ imageStripDepthTokens: 3500, // ~2 images (1600 each) fit within the window
71
+ });
72
+ const manager = await ContextManager.open({ path: TEST_STORE_PATH, strategy });
73
+ for (let i = 1; i <= 4; i++)
74
+ manager.addMessage('user', imageMessage(`img-0${i}`));
75
+ const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
76
+ // Newest fits inside the depth window; the oldest is well beyond it.
77
+ assert.equal(imageState(compiled.messages, 'img-04'), 'image', 'newest image within depth window');
78
+ assert.equal(imageState(compiled.messages, 'img-01'), 'stripped', 'oldest image beyond depth window');
79
+ // But the text of the stripped message is still present verbatim (text
80
+ // horizon is independent of the image horizon).
81
+ const oldestText = compiled.messages.find((m) => m.content.some((b) => b.type === 'text' && b.text.includes('img-01')));
82
+ assert.ok(oldestText, 'oldest message text survives even though its image was stripped');
83
+ manager.close();
84
+ });
85
+ it('leaves images untouched when the policy is disabled', async () => {
86
+ cleanup();
87
+ const strategy = new AutobiographicalStrategy({
88
+ headWindowTokens: 0,
89
+ recentWindowTokens: 1_000_000,
90
+ hierarchical: true,
91
+ maxLiveImages: 0,
92
+ imageStripDepthTokens: 0,
93
+ });
94
+ const manager = await ContextManager.open({ path: TEST_STORE_PATH, strategy });
95
+ for (let i = 1; i <= 3; i++)
96
+ manager.addMessage('user', imageMessage(`img-0${i}`));
97
+ const compiled = await manager.compile({ maxTokens: 1_000_000, reserveForResponse: 0 });
98
+ for (const label of ['img-01', 'img-02', 'img-03']) {
99
+ assert.equal(imageState(compiled.messages, label), 'image', `${label} kept when policy disabled`);
100
+ }
101
+ manager.close();
102
+ });
103
+ });
104
+ //# sourceMappingURL=image-stripping.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-stripping.test.js","sourceRoot":"","sources":["../../test/image-stripping.test.ts"],"names":[],"mappings":"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"}