pi-rtk-optimizer 0.7.1 → 0.8.1

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.
@@ -6,7 +6,7 @@ import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
6
6
 
7
7
  const TEST_AGENT_DIR = "/tmp/.pi/agent";
8
8
 
9
- mock.module("@mariozechner/pi-coding-agent", () => ({
9
+ mock.module("@earendil-works/pi-coding-agent", () => ({
10
10
  getAgentDir: () => TEST_AGENT_DIR,
11
11
  }));
12
12
 
@@ -75,6 +75,14 @@ function assertNoOutputEmoji(text: string): void {
75
75
  }
76
76
  }
77
77
 
78
+ function assertNoPartialHashlineAnchors(text: string): void {
79
+ for (const line of text.split(/\r?\n/)) {
80
+ if (/^\s*\d+\s*#[A-Za-z0-9_-]{2,32}:/.test(line)) {
81
+ assert.equal(line.endsWith("..."), false, `Anchor line was partially truncated: ${line}`);
82
+ }
83
+ }
84
+ }
85
+
78
86
  runTest("precision read with offset keeps exact output (no source/smart/hard truncation)", () => {
79
87
  const config = cloneDefaultConfig();
80
88
  setReadCompaction(config, true);
@@ -168,6 +176,203 @@ runTest("normal read compacts and adds banner when read compaction is enabled",
168
176
  assert.ok(compacted.includes("source:minimal"));
169
177
  });
170
178
 
179
+ runTest("line-anchor read output compacts without corrupting LINE#HASH anchors", () => {
180
+ const config = cloneDefaultConfig();
181
+ setReadCompaction(config, true);
182
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
183
+ config.outputCompaction.sourceCodeFiltering = "minimal";
184
+ config.outputCompaction.smartTruncate.enabled = true;
185
+ config.outputCompaction.smartTruncate.maxLines = 40;
186
+ config.outputCompaction.truncate.enabled = true;
187
+ config.outputCompaction.truncate.maxChars = 5000;
188
+
189
+ const content = Array.from({ length: 120 }, (_value, index) => {
190
+ const lineNumber = index + 1;
191
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
192
+ return `${String(lineNumber).padStart(3, " ")}#ZP:${sourceLine}`;
193
+ }).join("\n");
194
+ const result = compactToolResult(
195
+ {
196
+ toolName: "read",
197
+ input: { path: "sample.ts" },
198
+ content: [{ type: "text", text: content }],
199
+ },
200
+ config,
201
+ );
202
+
203
+ assert.equal(result.changed, true);
204
+ assert.ok(result.techniques.includes("source:minimal"));
205
+
206
+ const compacted = firstTextBlock(result.content);
207
+ assert.ok(compacted.startsWith("[RTK compacted output:"));
208
+ assert.ok(compacted.includes("source:minimal"));
209
+ assert.match(compacted, /\n\s*2#ZP:const value2 = 2;/);
210
+ assert.equal(compacted.includes("#ZP:// comment"), false);
211
+ assertNoPartialHashlineAnchors(compacted);
212
+ });
213
+
214
+ runTest("colon-pipe anchor read output compacts without requiring hashline extension", () => {
215
+ const config = cloneDefaultConfig();
216
+ setReadCompaction(config, true);
217
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
218
+ config.outputCompaction.sourceCodeFiltering = "minimal";
219
+ config.outputCompaction.smartTruncate.enabled = true;
220
+ config.outputCompaction.smartTruncate.maxLines = 40;
221
+ config.outputCompaction.truncate.enabled = true;
222
+ config.outputCompaction.truncate.maxChars = 5000;
223
+
224
+ const content = [
225
+ "Read sample.ts: 120 lines",
226
+ "",
227
+ ...Array.from({ length: 120 }, (_value, index) => {
228
+ const lineNumber = index + 1;
229
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
230
+ return `${lineNumber}:${(lineNumber % 256).toString(16).padStart(2, "0")}|${sourceLine}`;
231
+ }),
232
+ ].join("\n");
233
+ const result = compactToolResult(
234
+ {
235
+ toolName: "read",
236
+ input: { path: "sample.ts" },
237
+ content: [{ type: "text", text: content }],
238
+ },
239
+ config,
240
+ );
241
+
242
+ assert.equal(result.changed, true);
243
+ assert.ok(result.techniques.includes("source:minimal"));
244
+
245
+ const compacted = firstTextBlock(result.content);
246
+ assert.ok(compacted.includes("Read sample.ts: 120 lines"));
247
+ assert.match(compacted, /\n2:02\|const value2 = 2;/);
248
+ assert.equal(compacted.includes("|// comment"), false);
249
+ });
250
+
251
+ runTest("compact LINEHASH pipe anchors from oh-my-pi style reads", () => {
252
+ const config = cloneDefaultConfig();
253
+ setReadCompaction(config, true);
254
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
255
+ config.outputCompaction.sourceCodeFiltering = "minimal";
256
+ config.outputCompaction.smartTruncate.enabled = true;
257
+ config.outputCompaction.smartTruncate.maxLines = 40;
258
+ config.outputCompaction.truncate.enabled = true;
259
+ config.outputCompaction.truncate.maxChars = 5000;
260
+
261
+ const content = Array.from({ length: 120 }, (_value, index) => {
262
+ const lineNumber = index + 1;
263
+ const hash = lineNumber % 2 === 0 ? "sr" : "ab";
264
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
265
+ return `${lineNumber}${hash}|${sourceLine}`;
266
+ }).join("\n");
267
+ const result = compactToolResult(
268
+ {
269
+ toolName: "read",
270
+ input: { path: "sample.ts" },
271
+ content: [{ type: "text", text: content }],
272
+ },
273
+ config,
274
+ );
275
+
276
+ assert.equal(result.changed, true);
277
+ assert.ok(result.techniques.includes("source:minimal"));
278
+
279
+ const compacted = firstTextBlock(result.content);
280
+ assert.match(compacted, /\n2sr\|const value2 = 2;/);
281
+ assert.equal(compacted.includes("|// comment"), false);
282
+ });
283
+
284
+ runTest("compact hashline-tools file wrapper while preserving non-anchor wrapper lines", () => {
285
+ const config = cloneDefaultConfig();
286
+ setReadCompaction(config, true);
287
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
288
+ config.outputCompaction.sourceCodeFiltering = "minimal";
289
+ config.outputCompaction.smartTruncate.enabled = true;
290
+ config.outputCompaction.smartTruncate.maxLines = 40;
291
+ config.outputCompaction.truncate.enabled = true;
292
+ config.outputCompaction.truncate.maxChars = 5000;
293
+
294
+ const content = [
295
+ "<file>",
296
+ ...Array.from({ length: 120 }, (_value, index) => {
297
+ const lineNumber = index + 1;
298
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
299
+ return `${lineNumber}#ZM:${sourceLine}`;
300
+ }),
301
+ "",
302
+ "(End of file - 120 total lines)",
303
+ "</file>",
304
+ ].join("\n");
305
+ const result = compactToolResult(
306
+ {
307
+ toolName: "read",
308
+ input: { path: "sample.ts" },
309
+ content: [{ type: "text", text: content }],
310
+ },
311
+ config,
312
+ );
313
+
314
+ assert.equal(result.changed, true);
315
+ assert.ok(result.techniques.includes("source:minimal"));
316
+
317
+ const compacted = firstTextBlock(result.content);
318
+ assert.ok(compacted.includes("<file>"));
319
+ assert.ok(compacted.includes("(End of file - 120 total lines)"));
320
+ assert.ok(compacted.includes("</file>"));
321
+ assert.match(compacted, /\n2#ZM:const value2 = 2;/);
322
+ assert.equal(compacted.includes("#ZM:// comment"), false);
323
+ });
324
+
325
+ runTest("anchor-safe read hard truncation preserves whole hashline anchors", () => {
326
+ const config = cloneDefaultConfig();
327
+ setReadCompaction(config, true);
328
+ config.outputCompaction.sourceCodeFilteringEnabled = false;
329
+ config.outputCompaction.smartTruncate.enabled = false;
330
+ config.outputCompaction.truncate.enabled = true;
331
+ config.outputCompaction.truncate.maxChars = 350;
332
+
333
+ const content = Array.from({ length: 120 }, (_value, index) => {
334
+ const lineNumber = index + 1;
335
+ return `${lineNumber}#ZP:const value${lineNumber} = "${"x".repeat(40)}";`;
336
+ }).join("\n");
337
+ const result = compactToolResult(
338
+ {
339
+ toolName: "read",
340
+ input: { path: "sample.ts" },
341
+ content: [{ type: "text", text: content }],
342
+ },
343
+ config,
344
+ );
345
+
346
+ assert.equal(result.changed, true);
347
+ assert.ok(result.techniques.includes("truncate"));
348
+
349
+ const compacted = firstTextBlock(result.content);
350
+ assert.ok(compacted.includes("anchor-safe truncate"));
351
+ assertNoPartialHashlineAnchors(compacted);
352
+ });
353
+
354
+ runTest("incidental single anchor-like line does not disable normal read compaction", () => {
355
+ const config = cloneDefaultConfig();
356
+ setReadCompaction(config, true);
357
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
358
+ config.outputCompaction.sourceCodeFiltering = "minimal";
359
+ config.outputCompaction.smartTruncate.enabled = true;
360
+ config.outputCompaction.smartTruncate.maxLines = 40;
361
+
362
+ const content = [`1#ZP:not an anchored read`, buildReadContent(120)].join("\n");
363
+ const result = compactToolResult(
364
+ {
365
+ toolName: "read",
366
+ input: { path: "sample.ts" },
367
+ content: [{ type: "text", text: content }],
368
+ },
369
+ config,
370
+ );
371
+
372
+ assert.equal(result.changed, true);
373
+ assert.ok(result.techniques.includes("source:minimal") || result.techniques.includes("smart-truncate"));
374
+ });
375
+
171
376
  runTest("short read output stays exact below threshold", () => {
172
377
  const config = cloneDefaultConfig();
173
378
  setReadCompaction(config, true);
@@ -435,7 +640,7 @@ runTest("rtk grep-style output sanitizes emoji file markers", () => {
435
640
  runTest("rtk git diff verbose summary sanitizes file markers", () => {
436
641
  const compacted = compactBashOutput(
437
642
  "rtk git diff -- agent/extensions/pi-mcp-adapter/package.json",
438
- "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@mariozechner/pi-coding-agent\": \"^0.58.1\",\n",
643
+ "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@earendil-works/pi-coding-agent\": \"^0.58.1\",\n",
439
644
  );
440
645
 
441
646
  assert.ok(compacted.includes("--- Changes ---"));
@@ -446,7 +651,7 @@ runTest("rtk git diff verbose summary sanitizes file markers", () => {
446
651
  runTest("git diff compaction skips already-compacted RTK-shaped output", () => {
447
652
  const compacted = compactBashOutput(
448
653
  "git diff -- agent/extensions/pi-mcp-adapter/package.json",
449
- "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@mariozechner/pi-coding-agent\": \"^0.58.1\",\n",
654
+ "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@earendil-works/pi-coding-agent\": \"^0.58.1\",\n",
450
655
  );
451
656
 
452
657
  assert.ok(compacted.includes("--- Changes ---"));
@@ -1,4 +1,4 @@
1
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
1
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
4
  import {
@@ -48,6 +48,24 @@ export interface ToolResultCompactionOutcome {
48
48
  metadata?: ToolResultCompactionMetadata;
49
49
  }
50
50
 
51
+ interface AnchoredReadLine {
52
+ lineNumber: number;
53
+ content: string;
54
+ originalLine: string;
55
+ }
56
+
57
+ interface AnchorSafeReadLine {
58
+ text: string;
59
+ content: string;
60
+ }
61
+
62
+ interface AnchorSafeReadParts {
63
+ prefixLines: string[];
64
+ anchoredLines: AnchoredReadLine[];
65
+ suffixLines: string[];
66
+ trailingNewline: boolean;
67
+ }
68
+
51
69
  const LOSSY_TECHNIQUE_PREFIXES = [
52
70
  "build",
53
71
  "test",
@@ -61,6 +79,15 @@ const LOSSY_TECHNIQUE_PREFIXES = [
61
79
 
62
80
  const READ_EXACT_OUTPUT_LINE_THRESHOLD = 80;
63
81
  const READ_COMPACTION_BANNER_PREFIX = "[RTK compacted output:";
82
+ const ANCHORED_READ_LINE_MIN_MATCHES = 2;
83
+ const ANCHORED_READ_LINE_MIN_RATIO = 0.5;
84
+ const ANCHORED_READ_LINE_SAMPLE_LIMIT = 200;
85
+ const ANCHORED_READ_LINE_PATTERNS = [
86
+ /^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)\s*#\s*[A-Za-z0-9_-]{2,32}:(.*)$/,
87
+ /^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)\s*:\s*[A-Za-z0-9_-]{1,32}\|(.*)$/,
88
+ /^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)[a-z]{2}\|(.*)$/,
89
+ ] as const;
90
+ const ANCHORED_READ_INFORMATIONAL_LINE_PATTERN = /^\s*(?:$|<\/?file>|\.{3}|\[[^\]]+\]|Read\s+.+:\s+\d+\s+lines\b)/;
64
91
  const USER_SKILL_ROOTS = [join(getAgentDir(), "skills"), join(homedir(), ".agents", "skills")];
65
92
 
66
93
  function normalizePathForComparison(path: string): string {
@@ -135,6 +162,87 @@ function hasExplicitReadRange(input: Record<string, unknown>): boolean {
135
162
  return input.offset !== undefined || input.limit !== undefined;
136
163
  }
137
164
 
165
+ function splitReadLines(text: string): { lines: string[]; trailingNewline: boolean } {
166
+ if (!text) {
167
+ return { lines: [], trailingNewline: false };
168
+ }
169
+
170
+ const trailingNewline = text.endsWith("\n");
171
+ const lines = text.split(/\r?\n/);
172
+ if (trailingNewline) {
173
+ lines.pop();
174
+ }
175
+
176
+ return { lines, trailingNewline };
177
+ }
178
+
179
+ function joinReadLines(lines: string[], trailingNewline: boolean): string {
180
+ const joined = lines.join("\n");
181
+ return trailingNewline && joined ? `${joined}\n` : joined;
182
+ }
183
+
184
+ function parseAnchoredReadLine(line: string): AnchoredReadLine | undefined {
185
+ for (const pattern of ANCHORED_READ_LINE_PATTERNS) {
186
+ const match = line.match(pattern);
187
+ if (!match) {
188
+ continue;
189
+ }
190
+
191
+ const lineNumber = Number.parseInt(match[1] ?? "", 10);
192
+ if (!Number.isSafeInteger(lineNumber) || lineNumber <= 0) {
193
+ continue;
194
+ }
195
+
196
+ const content = match[2] ?? "";
197
+ return {
198
+ lineNumber,
199
+ content,
200
+ originalLine: line,
201
+ };
202
+ }
203
+
204
+ return undefined;
205
+ }
206
+
207
+ function parseAnchoredReadLineNumber(line: string): number | undefined {
208
+ return parseAnchoredReadLine(line)?.lineNumber;
209
+ }
210
+
211
+ function looksLikeAnchoredLineOutput(text: string, parseLineNumber: (line: string) => number | undefined): boolean {
212
+ let matchCount = 0;
213
+ let relevantLineCount = 0;
214
+ let previousMatchedLineNumber: number | undefined;
215
+ let hasIncreasingAnchors = false;
216
+
217
+ for (const line of splitReadLines(text).lines.slice(0, ANCHORED_READ_LINE_SAMPLE_LIMIT)) {
218
+ if (!ANCHORED_READ_INFORMATIONAL_LINE_PATTERN.test(line)) {
219
+ relevantLineCount += 1;
220
+ }
221
+
222
+ const lineNumber = parseLineNumber(line);
223
+ if (lineNumber === undefined) {
224
+ continue;
225
+ }
226
+
227
+ matchCount += 1;
228
+ if (previousMatchedLineNumber !== undefined && lineNumber > previousMatchedLineNumber) {
229
+ hasIncreasingAnchors = true;
230
+ }
231
+ previousMatchedLineNumber = lineNumber;
232
+ }
233
+
234
+ if (matchCount < ANCHORED_READ_LINE_MIN_MATCHES || !hasIncreasingAnchors) {
235
+ return false;
236
+ }
237
+
238
+ const ratioBase = Math.max(relevantLineCount, matchCount);
239
+ return matchCount / ratioBase >= ANCHORED_READ_LINE_MIN_RATIO;
240
+ }
241
+
242
+ function looksLikeAnchoredReadOutput(text: string): boolean {
243
+ return looksLikeAnchoredLineOutput(text, parseAnchoredReadLineNumber);
244
+ }
245
+
138
246
  function shouldPreserveExactReadOutput(
139
247
  text: string,
140
248
  input: Record<string, unknown>,
@@ -165,6 +273,179 @@ function shouldApplyReadSourceFiltering(text: string, config: RtkIntegrationConf
165
273
  );
166
274
  }
167
275
 
276
+ function extractAnchoredReadParts(text: string): AnchorSafeReadParts | undefined {
277
+ if (!looksLikeAnchoredReadOutput(text)) {
278
+ return undefined;
279
+ }
280
+
281
+ const { lines, trailingNewline } = splitReadLines(text);
282
+ const parsedLines = lines.map((line) => parseAnchoredReadLine(line));
283
+ const firstAnchorIndex = parsedLines.findIndex((line) => line !== undefined);
284
+ if (firstAnchorIndex === -1) {
285
+ return undefined;
286
+ }
287
+
288
+ let lastAnchorIndex = firstAnchorIndex;
289
+ for (let index = parsedLines.length - 1; index >= firstAnchorIndex; index -= 1) {
290
+ if (parsedLines[index] !== undefined) {
291
+ lastAnchorIndex = index;
292
+ break;
293
+ }
294
+ }
295
+
296
+ const anchoredLines: AnchoredReadLine[] = [];
297
+ for (let index = firstAnchorIndex; index <= lastAnchorIndex; index += 1) {
298
+ const anchoredLine = parsedLines[index];
299
+ if (!anchoredLine) {
300
+ return undefined;
301
+ }
302
+ anchoredLines.push(anchoredLine);
303
+ }
304
+
305
+ return {
306
+ prefixLines: lines.slice(0, firstAnchorIndex),
307
+ anchoredLines,
308
+ suffixLines: lines.slice(lastAnchorIndex + 1),
309
+ trailingNewline,
310
+ };
311
+ }
312
+
313
+ function toAnchorSafeReadLines(anchoredLines: AnchoredReadLine[]): AnchorSafeReadLine[] {
314
+ return anchoredLines.map((line) => ({
315
+ text: line.originalLine,
316
+ content: line.content,
317
+ }));
318
+ }
319
+
320
+ function renderAnchorSafeReadBody(lines: AnchorSafeReadLine[]): string {
321
+ return lines.map((line) => line.text).join("\n");
322
+ }
323
+
324
+ function renderAnchorSafeReadText(parts: AnchorSafeReadParts, lines: AnchorSafeReadLine[]): string {
325
+ return joinReadLines(
326
+ [...parts.prefixLines, ...lines.map((line) => line.text), ...parts.suffixLines],
327
+ parts.trailingNewline,
328
+ );
329
+ }
330
+
331
+ function remapTransformedContentToAnchorSafeLines(
332
+ sourceLines: AnchorSafeReadLine[],
333
+ transformedContent: string,
334
+ ): AnchorSafeReadLine[] {
335
+ const transformedLines = splitReadLines(transformedContent).lines;
336
+ const remappedLines: AnchorSafeReadLine[] = [];
337
+ let searchStartIndex = 0;
338
+
339
+ for (const transformedLine of transformedLines) {
340
+ let matchedIndex = -1;
341
+ for (let index = searchStartIndex; index < sourceLines.length; index += 1) {
342
+ if (sourceLines[index]?.content === transformedLine) {
343
+ matchedIndex = index;
344
+ break;
345
+ }
346
+ }
347
+
348
+ if (matchedIndex === -1) {
349
+ remappedLines.push({
350
+ text: transformedLine,
351
+ content: transformedLine,
352
+ });
353
+ continue;
354
+ }
355
+
356
+ remappedLines.push(sourceLines[matchedIndex]!);
357
+ searchStartIndex = matchedIndex + 1;
358
+ }
359
+
360
+ return remappedLines;
361
+ }
362
+
363
+ function truncateAnchorSafeReadLines(lines: AnchorSafeReadLine[], maxChars: number): AnchorSafeReadLine[] {
364
+ if (renderAnchorSafeReadBody(lines).length <= maxChars) {
365
+ return lines;
366
+ }
367
+
368
+ const marker = "[RTK anchor-safe truncate: remaining anchored read lines omitted to preserve complete anchors]";
369
+ const truncatedLines: AnchorSafeReadLine[] = [];
370
+ let charCount = 0;
371
+
372
+ for (let index = 0; index < lines.length; index += 1) {
373
+ const line = lines[index]!;
374
+ const separatorLength = truncatedLines.length > 0 ? 1 : 0;
375
+ const nextCharCount = charCount + separatorLength + line.text.length;
376
+ const remainingAfter = lines.length - index - 1;
377
+ const markerLength = remainingAfter > 0 ? (nextCharCount > 0 ? 1 : 0) + marker.length : 0;
378
+
379
+ if (nextCharCount + markerLength > maxChars) {
380
+ const markerLine = { text: marker, content: marker };
381
+ return truncatedLines.length > 0 ? [...truncatedLines, markerLine] : [markerLine];
382
+ }
383
+
384
+ truncatedLines.push(line);
385
+ charCount = nextCharCount;
386
+ }
387
+
388
+ return truncatedLines;
389
+ }
390
+
391
+ function compactAnchoredReadText(
392
+ text: string,
393
+ filePath: string,
394
+ config: RtkIntegrationConfig,
395
+ ): { text: string; techniques: string[] } {
396
+ const parts = extractAnchoredReadParts(text);
397
+ if (!parts) {
398
+ return { text, techniques: [] };
399
+ }
400
+
401
+ let lines = toAnchorSafeReadLines(parts.anchoredLines);
402
+ const techniques: string[] = [];
403
+ const compaction = config.outputCompaction;
404
+ const language = detectLanguage(filePath);
405
+
406
+ if (
407
+ compaction.sourceCodeFilteringEnabled &&
408
+ compaction.sourceCodeFiltering !== "none" &&
409
+ shouldApplyReadSourceFiltering(text, config)
410
+ ) {
411
+ const currentSource = lines.map((line) => line.content).join("\n");
412
+ const filtered = normalizeTechniqueResult(
413
+ filterSourceCode(currentSource, language, compaction.sourceCodeFiltering),
414
+ currentSource,
415
+ );
416
+ const filteredLines = remapTransformedContentToAnchorSafeLines(lines, filtered);
417
+ if (renderAnchorSafeReadBody(filteredLines) !== renderAnchorSafeReadBody(lines)) {
418
+ lines = filteredLines;
419
+ techniques.push(`source:${compaction.sourceCodeFiltering}`);
420
+ }
421
+ }
422
+
423
+ if (compaction.smartTruncate.enabled && lines.length > compaction.smartTruncate.maxLines) {
424
+ const currentSource = lines.map((line) => line.content).join("\n");
425
+ const compacted = smartTruncate(currentSource, compaction.smartTruncate.maxLines, language);
426
+ const compactedLines = remapTransformedContentToAnchorSafeLines(lines, compacted);
427
+ if (renderAnchorSafeReadBody(compactedLines) !== renderAnchorSafeReadBody(lines)) {
428
+ lines = compactedLines;
429
+ techniques.push("smart-truncate");
430
+ }
431
+ }
432
+
433
+ if (compaction.truncate.enabled && renderAnchorSafeReadText(parts, lines).length > compaction.truncate.maxChars) {
434
+ const nonBodyOverhead = renderAnchorSafeReadText(parts, []).length;
435
+ const bodyMaxChars = Math.max(1, compaction.truncate.maxChars - nonBodyOverhead);
436
+ const truncatedLines = truncateAnchorSafeReadLines(lines, bodyMaxChars);
437
+ if (renderAnchorSafeReadBody(truncatedLines) !== renderAnchorSafeReadBody(lines)) {
438
+ lines = truncatedLines;
439
+ techniques.push("truncate");
440
+ }
441
+ }
442
+
443
+ return {
444
+ text: renderAnchorSafeReadText(parts, lines),
445
+ techniques,
446
+ };
447
+ }
448
+
168
449
  function formatReadCompactionBanner(techniques: string[]): string {
169
450
  return `${READ_COMPACTION_BANNER_PREFIX} ${techniques.join(", ")}]`;
170
451
  }
@@ -285,6 +566,18 @@ function compactReadText(
285
566
  }
286
567
  }
287
568
 
569
+ if (looksLikeAnchoredReadOutput(nextText)) {
570
+ const anchored = compactAnchoredReadText(nextText, filePath, config);
571
+ nextText = anchored.text;
572
+ techniques.push(...anchored.techniques);
573
+
574
+ if (techniques.length > 0 && !nextText.startsWith(READ_COMPACTION_BANNER_PREFIX)) {
575
+ nextText = `${formatReadCompactionBanner(techniques)}\n${nextText}`;
576
+ }
577
+
578
+ return { text: nextText, techniques };
579
+ }
580
+
288
581
  const language = detectLanguage(filePath);
289
582
  // Only apply lossy source filtering when a downstream line/char safeguard would otherwise trigger.
290
583
  if (