@kolisachint/hoocode-agent 0.5.49 → 0.5.50
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.
- package/CHANGELOG.md +2 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +326 -101
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/read-dedup.d.ts +14 -1
- package/dist/core/tools/read-dedup.d.ts.map +1 -1
- package/dist/core/tools/read-dedup.js +53 -1
- package/dist/core/tools/read-dedup.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +17 -5
- package/dist/core/tools/read.js.map +1 -1
- package/dist/extensions/core/permission-gate.d.ts.map +1 -1
- package/dist/extensions/core/permission-gate.js +23 -5
- package/dist/extensions/core/permission-gate.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n */\n\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\n/** Cache for normalized text to avoid redundant processing. Max 100 entries. */\nconst normalizeCache = new Map<string, string>();\nconst MAX_CACHE_SIZE = 100;\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching. Applies progressive transformations:\n * - Normalize line endings to LF\n * - Strip trailing whitespace from each line\n * - Normalize tabs to spaces (2 spaces per tab)\n * - Collapse multiple spaces to single space\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nfunction normalizeForFuzzyMatch(text: string): string {\n\t// Check cache first\n\tconst cached = normalizeCache.get(text);\n\tif (cached !== undefined) return cached;\n\n\tconst normalized = text\n\t\t.normalize(\"NFKC\")\n\t\t// Normalize line endings to LF\n\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t.replace(/\\r/g, \"\\n\")\n\t\t// Strip trailing whitespace per line\n\t\t.split(\"\\n\")\n\t\t.map((line) => {\n\t\t\t// Normalize tabs to 2 spaces\n\t\t\tlet normalized = line.replace(/\\t/g, \" \");\n\t\t\t// Collapse multiple spaces to single space (but preserve leading indentation pattern)\n\t\t\t// Only collapse spaces that are NOT at the start of the line (indentation)\n\t\t\tconst leadingSpaces = normalized.match(/^(\\s*)/)?.[1] ?? \"\";\n\t\t\tconst rest = normalized.slice(leadingSpaces.length);\n\t\t\tnormalized = leadingSpaces + rest.replace(/ {2,}/g, \" \");\n\t\t\treturn normalized.trimEnd();\n\t\t})\n\t\t.join(\"\\n\")\n\t\t// Smart single quotes → '\n\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t// Smart double quotes → \"\n\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t// Various dashes/hyphens → -\n\t\t// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,\n\t\t// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus\n\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t// Special spaces → regular space\n\t\t// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,\n\t\t// U+205F medium math space, U+3000 ideographic space\n\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \");\n\n\t// Cache the result (with size limit)\n\tif (normalizeCache.size >= MAX_CACHE_SIZE) {\n\t\t// Remove oldest entry (first key)\n\t\tconst firstKey = normalizeCache.keys().next().value;\n\t\tif (firstKey !== undefined) {\n\t\t\tnormalizeCache.delete(firstKey);\n\t\t}\n\t}\n\tnormalizeCache.set(text, normalized);\n\n\treturn normalized;\n}\n\n/**\n * Line starts and line texts for one string. Each entry in `lines` includes its\n * own trailing newline, so `starts[i] + lines[i].length` is the start of line\n * `i + 1`. Used to translate a fuzzy match back into original coordinates.\n */\ninterface LineIndex {\n\ttext: string;\n\tstarts: number[];\n\tlines: string[];\n}\n\nfunction buildLineIndex(text: string): LineIndex {\n\tconst starts: number[] = [];\n\tconst lines: string[] = [];\n\tlet start = 0;\n\tfor (;;) {\n\t\tconst newline = text.indexOf(\"\\n\", start);\n\t\tif (newline === -1) {\n\t\t\tstarts.push(start);\n\t\t\tlines.push(text.slice(start));\n\t\t\treturn { text, starts, lines };\n\t\t}\n\t\tstarts.push(start);\n\t\tlines.push(text.slice(start, newline + 1));\n\t\tstart = newline + 1;\n\t}\n}\n\n/** Index of the line containing `offset` (greatest `i` with `starts[i] <= offset`). */\nfunction lineIndexAt(starts: number[], offset: number): number {\n\tlet low = 0;\n\tlet high = starts.length - 1;\n\twhile (low < high) {\n\t\tconst mid = (low + high + 1) >> 1;\n\t\tif (starts[mid] <= offset) low = mid;\n\t\telse high = mid - 1;\n\t}\n\treturn low;\n}\n\nexport interface Edit {\n\toldText: string;\n\tnewText: string;\n\t/**\n\t * When true, replace every occurrence of oldText instead of requiring it to\n\t * be unique. Default (false/undefined) keeps the uniqueness guardrail.\n\t */\n\treplaceAll?: boolean;\n}\n\ninterface MatchedEdit {\n\teditIndex: number;\n\tmatchIndex: number;\n\tmatchLength: number;\n\t/**\n\t * Text written over the span. Usually the edit's `newText` verbatim; for a\n\t * fuzzy match that covered only part of a line, the untouched remainder of\n\t * the first/last line rides along in normalized form (see `resolveFuzzySpan`).\n\t */\n\treplacement: string;\n}\n\n/** A match located in, and expressed in coordinates of, the original content. */\ninterface ResolvedSpan {\n\tmatchIndex: number;\n\tmatchLength: number;\n\treplacement: string;\n}\n\nexport interface AppliedEditsResult {\n\tbaseContent: string;\n\tnewContent: string;\n}\n\n/**\n * Translate a match found in fuzzy-normalized space back into a span of the\n * original content.\n *\n * Normalization never adds or removes a newline, so normalized line N always\n * corresponds to original line N; only columns within a line can shift (a tab\n * widens to two spaces, a run of spaces collapses, NFKC changes a character's\n * length). Rather than track per-character offsets through those transforms,\n * the span is widened to whole original lines, and whatever of the first and\n * last line the match did not cover is re-attached around `newText` in\n * normalized form.\n *\n * The practical effect: a match that covers whole lines - by far the common\n * case - rewrites exactly those lines and leaves every other byte of the file\n * untouched. Only a partial-line fuzzy match normalizes anything the edit did\n * not ask for, and never beyond the lines it landed on.\n */\nfunction resolveFuzzySpans(\n\toriginal: LineIndex,\n\tnormalized: LineIndex,\n\tnormStarts: number[],\n\tnormLength: number,\n\tnewText: string,\n): ResolvedSpan[] {\n\tconst spans: ResolvedSpan[] = [];\n\tconst lineOf = (offset: number) => lineIndexAt(normalized.starts, offset);\n\n\tlet i = 0;\n\twhile (i < normStarts.length) {\n\t\tconst firstLine = lineOf(normStarts[i]);\n\t\tlet lastLine = lineOf(normStarts[i] + normLength - 1);\n\n\t\t// Matches are ascending and non-overlapping. Absorb any that land inside\n\t\t// the same line block so the block is emitted once with every\n\t\t// substitution applied, rather than as colliding same-line spans.\n\t\tlet end = i + 1;\n\t\twhile (end < normStarts.length && lineOf(normStarts[end]) <= lastLine) {\n\t\t\tlastLine = Math.max(lastLine, lineOf(normStarts[end] + normLength - 1));\n\t\t\tend++;\n\t\t}\n\n\t\tconst blockStart = normalized.starts[firstLine];\n\t\tconst blockEnd = normalized.starts[lastLine] + normalized.lines[lastLine].length;\n\t\tlet replacement = \"\";\n\t\tlet cursor = blockStart;\n\t\tfor (let k = i; k < end; k++) {\n\t\t\treplacement += normalized.text.slice(cursor, normStarts[k]) + newText;\n\t\t\tcursor = normStarts[k] + normLength;\n\t\t}\n\t\treplacement += normalized.text.slice(cursor, blockEnd);\n\n\t\tconst matchIndex = original.starts[firstLine];\n\t\tconst matchEnd = original.starts[lastLine] + original.lines[lastLine].length;\n\t\tspans.push({ matchIndex, matchLength: matchEnd - matchIndex, replacement });\n\t\ti = end;\n\t}\n\n\treturn spans;\n}\n\n/**\n * Locate one edit's `oldText` in `content`, always returning spans in\n * `content`'s own coordinates.\n *\n * Three tiers, tried in order: exact, fuzzy-normalized, then the\n * indentation-tolerant line-block fallback. `occurrences` counts the matches the\n * winning tier found, which is what the uniqueness guardrail tests; `spans` can\n * be shorter, because two fuzzy matches sharing a line are emitted as one\n * rewrite of that line.\n */\nfunction findEditSpans(\n\tcontent: string,\n\tedit: { oldText: string; newText: string },\n\tlineIndexes: () => { original: LineIndex; normalized: LineIndex } | null,\n): { spans: ResolvedSpan[]; occurrences: number } {\n\t// Tier 1: exact. Already in original coordinates.\n\tconst exact = collectMatchIndices(content, edit.oldText);\n\tif (exact.length > 0) {\n\t\treturn {\n\t\t\tspans: exact.map((matchIndex) => ({\n\t\t\t\tmatchIndex,\n\t\t\t\tmatchLength: edit.oldText.length,\n\t\t\t\treplacement: edit.newText,\n\t\t\t})),\n\t\t\toccurrences: exact.length,\n\t\t};\n\t}\n\n\t// Tier 2: fuzzy. Located in normalized space, then translated back.\n\tconst indexes = lineIndexes();\n\tif (indexes) {\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(edit.oldText);\n\t\tconst fuzzy = collectMatchIndices(indexes.normalized.text, fuzzyOldText);\n\t\tif (fuzzy.length > 0) {\n\t\t\t// oldText did not match the file byte-for-byte, so the whitespace the model\n\t\t\t// used is its own rendering rather than a statement about the file. If\n\t\t\t// newText normalizes to the same thing, it asked for no change: leave the\n\t\t\t// span exactly as it is so the no-change error fires, instead of rewriting\n\t\t\t// the line's indentation to match the model's rendering of it.\n\t\t\tif (normalizeForFuzzyMatch(edit.newText) === fuzzyOldText) {\n\t\t\t\treturn {\n\t\t\t\t\tspans: resolveFuzzySpans(\n\t\t\t\t\t\tindexes.original,\n\t\t\t\t\t\tindexes.normalized,\n\t\t\t\t\t\tfuzzy,\n\t\t\t\t\t\tfuzzyOldText.length,\n\t\t\t\t\t\tedit.newText,\n\t\t\t\t\t).map((span) => ({\n\t\t\t\t\t\t...span,\n\t\t\t\t\t\treplacement: content.slice(span.matchIndex, span.matchIndex + span.matchLength),\n\t\t\t\t\t})),\n\t\t\t\t\toccurrences: fuzzy.length,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tspans: resolveFuzzySpans(indexes.original, indexes.normalized, fuzzy, fuzzyOldText.length, edit.newText),\n\t\t\t\toccurrences: fuzzy.length,\n\t\t\t};\n\t\t}\n\t}\n\n\t// Tier 3: indentation-tolerant line blocks. Already in original coordinates.\n\tconst blocks = findLineBlockMatches(content, edit.oldText).map((span) => ({\n\t\tmatchIndex: span.matchIndex,\n\t\tmatchLength: span.matchLength,\n\t\treplacement: edit.newText,\n\t}));\n\treturn { spans: blocks, occurrences: blocks.length };\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Collect the start index of every non-overlapping occurrence of needle in\n * haystack. The needle must already be in the same space as haystack (raw for\n * exact matches, fuzzy-normalized when haystack is fuzzy-normalized).\n */\nfunction collectMatchIndices(haystack: string, needle: string): number[] {\n\tconst indices: number[] = [];\n\tif (needle.length === 0) return indices;\n\tlet from = 0;\n\twhile (true) {\n\t\tconst idx = haystack.indexOf(needle, from);\n\t\tif (idx === -1) break;\n\t\tindices.push(idx);\n\t\tfrom = idx + needle.length; // non-overlapping\n\t}\n\treturn indices;\n}\n\n/**\n * Per-line normalization for indentation-tolerant block matching. Applies the\n * same Unicode/space normalization as fuzzy matching, then strips all leading\n * and trailing whitespace so indentation differences are ignored entirely.\n */\nfunction blockNormalizeLine(line: string): string {\n\treturn normalizeForFuzzyMatch(line).trim();\n}\n\ninterface LineBlockMatch {\n\tmatchIndex: number;\n\tmatchLength: number;\n}\n\n/**\n * Indentation-tolerant fallback matcher. Compares oldText against content line\n * by line, ignoring each line's leading/trailing whitespace (and Unicode\n * formatting). Returns character spans in `content` for every block whose\n * trimmed lines equal the trimmed oldText lines. Replacement still happens in\n * the original content space, so surrounding formatting is preserved.\n */\nfunction findLineBlockMatches(content: string, oldText: string): LineBlockMatch[] {\n\tconst hadTrailingNewline = oldText.endsWith(\"\\n\");\n\tconst oldLines = oldText.split(\"\\n\");\n\tif (hadTrailingNewline) oldLines.pop();\n\tif (oldLines.length === 0) return [];\n\tconst trimmedOld = oldLines.map(blockNormalizeLine);\n\n\tconst contentLines = content.split(\"\\n\");\n\tconst k = trimmedOld.length;\n\tif (k > contentLines.length) return [];\n\n\t// Char offset of each line start within content.\n\tconst offsets = new Array<number>(contentLines.length);\n\tlet acc = 0;\n\tfor (let i = 0; i < contentLines.length; i++) {\n\t\toffsets[i] = acc;\n\t\tacc += contentLines[i].length + 1; // + newline\n\t}\n\n\tconst matches: LineBlockMatch[] = [];\n\tfor (let i = 0; i + k <= contentLines.length; i++) {\n\t\tlet ok = true;\n\t\tfor (let j = 0; j < k; j++) {\n\t\t\tif (blockNormalizeLine(contentLines[i + j]) !== trimmedOld[j]) {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!ok) continue;\n\t\tconst matchIndex = offsets[i];\n\t\tlet matchLength = 0;\n\t\tfor (let j = 0; j < k; j++) matchLength += contentLines[i + j].length + (j < k - 1 ? 1 : 0);\n\t\t// Include the trailing newline when oldText carried one and a line follows the block.\n\t\tif (hadTrailingNewline && i + k < contentLines.length) matchLength += 1;\n\t\tmatches.push({ matchIndex, matchLength });\n\t}\n\treturn matches;\n}\n\nfunction getNotFoundError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`,\n\t);\n}\n\nfunction getDuplicateError(path: string, editIndex: number, totalEdits: number, occurrences: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`,\n\t);\n}\n\nfunction getEmptyOldTextError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(`oldText must not be empty in ${path}.`);\n\t}\n\treturn new Error(`edits[${editIndex}].oldText must not be empty in ${path}.`);\n}\n\nfunction getNoChangeError(path: string, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,\n\t\t);\n\t}\n\treturn new Error(`No changes made to ${path}. The replacements produced identical content.`);\n}\n\n/**\n * Apply one or more exact-text replacements to LF-normalized content.\n *\n * All edits are matched against the same original content. Replacements are\n * then applied in reverse order so offsets remain stable.\n *\n * Every match, however loosely it was found, is resolved back to a span of the\n * *original* content before anything is written. A fuzzy or indentation-tolerant\n * match therefore rewrites only the lines it landed on: the rest of the file\n * keeps its exact bytes, and the diff callers render from `baseContent` is the\n * real change on disk rather than a normalized-vs-normalized view of it.\n */\nexport function applyEditsToNormalizedContent(\n\tnormalizedContent: string,\n\tedits: Edit[],\n\tpath: string,\n): AppliedEditsResult {\n\tconst normalizedEdits = edits.map((edit) => ({\n\t\toldText: normalizeToLF(edit.oldText),\n\t\tnewText: normalizeToLF(edit.newText),\n\t\treplaceAll: edit.replaceAll === true,\n\t}));\n\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tif (normalizedEdits[i].oldText.length === 0) {\n\t\t\tthrow getEmptyOldTextError(path, i, normalizedEdits.length);\n\t\t}\n\t}\n\n\tconst baseContent = normalizedContent;\n\n\t// Both line indexes are needed only if some edit reaches the fuzzy tier, and\n\t// they are identical for every edit, so build them at most once. Normalization\n\t// is line-preserving; if that ever fails to hold, the fuzzy tier is skipped\n\t// rather than risk translating a span against a mismatched line table.\n\tlet lineIndexesCache: { original: LineIndex; normalized: LineIndex } | null | undefined;\n\tconst lineIndexes = () => {\n\t\tif (lineIndexesCache === undefined) {\n\t\t\tconst original = buildLineIndex(baseContent);\n\t\t\tconst normalized = buildLineIndex(normalizeForFuzzyMatch(baseContent));\n\t\t\tlineIndexesCache = original.lines.length === normalized.lines.length ? { original, normalized } : null;\n\t\t}\n\t\treturn lineIndexesCache;\n\t};\n\n\tconst matchedEdits: MatchedEdit[] = [];\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tconst edit = normalizedEdits[i];\n\t\tconst { spans, occurrences } = findEditSpans(baseContent, edit, lineIndexes);\n\t\tif (spans.length === 0) {\n\t\t\tthrow getNotFoundError(path, i, normalizedEdits.length);\n\t\t}\n\n\t\tif (edit.replaceAll) {\n\t\t\t// Replace every occurrence so the shared reverse-order applier rewrites them all.\n\t\t\tfor (const span of spans) {\n\t\t\t\tmatchedEdits.push({\n\t\t\t\t\teditIndex: i,\n\t\t\t\t\tmatchIndex: span.matchIndex,\n\t\t\t\t\tmatchLength: span.matchLength,\n\t\t\t\t\treplacement: span.replacement,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (occurrences > 1) {\n\t\t\tthrow getDuplicateError(path, i, normalizedEdits.length, occurrences);\n\t\t}\n\n\t\tmatchedEdits.push({\n\t\t\teditIndex: i,\n\t\t\tmatchIndex: spans[0].matchIndex,\n\t\t\tmatchLength: spans[0].matchLength,\n\t\t\treplacement: spans[0].replacement,\n\t\t});\n\t}\n\n\tmatchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);\n\tfor (let i = 1; i < matchedEdits.length; i++) {\n\t\tconst previous = matchedEdits[i - 1];\n\t\tconst current = matchedEdits[i];\n\t\tif (previous.matchIndex + previous.matchLength > current.matchIndex) {\n\t\t\tthrow new Error(\n\t\t\t\t`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tlet newContent = baseContent;\n\tfor (let i = matchedEdits.length - 1; i >= 0; i--) {\n\t\tconst edit = matchedEdits[i];\n\t\tnewContent =\n\t\t\tnewContent.substring(0, edit.matchIndex) +\n\t\t\tedit.replacement +\n\t\t\tnewContent.substring(edit.matchIndex + edit.matchLength);\n\t}\n\n\tif (baseContent === newContent) {\n\t\tthrow getNoChangeError(path, normalizedEdits.length);\n\t}\n\n\treturn { baseContent, newContent };\n}\n\n/**\n * Generate a unified diff string with line numbers and context.\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\tlet firstChangedLine: number | undefined;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Capture the first changed line (in the new file)\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLineNum;\n\t\t\t}\n\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\t\t\tconst hasLeadingChange = lastWasChange;\n\t\t\tconst hasTrailingChange = nextPartIsChange;\n\n\t\t\tif (hasLeadingChange && hasTrailingChange) {\n\t\t\t\tif (raw.length <= contextLines * 2) {\n\t\t\t\t\tfor (const line of raw) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst leadingLines = raw.slice(0, contextLines);\n\t\t\t\t\tconst trailingLines = raw.slice(raw.length - contextLines);\n\t\t\t\t\tconst skippedLines = raw.length - leadingLines.length - trailingLines.length;\n\n\t\t\t\t\tfor (const line of leadingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\n\t\t\t\t\tfor (const line of trailingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasLeadingChange) {\n\t\t\t\tconst shownLines = raw.slice(0, contextLines);\n\t\t\t\tconst skippedLines = raw.length - shownLines.length;\n\n\t\t\t\tfor (const line of shownLines) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\t\t\t} else if (hasTrailingChange) {\n\t\t\t\tconst skippedLines = Math.max(0, raw.length - contextLines);\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\n\t\t\t\tfor (const line of raw.slice(skippedLines)) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn { diff: output.join(\"\\n\"), firstChangedLine };\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for one or more edit operations without applying them.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditsDiff(\n\tpath: string,\n\tedits: Edit[],\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\treturn { error: `Could not edit file: ${path}. ${errorMessage}.` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"edit-diff.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n */\n\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\n/** Cache for normalized text to avoid redundant processing. Max 100 entries. */\nconst normalizeCache = new Map<string, string>();\nconst MAX_CACHE_SIZE = 100;\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * A normalized string plus, for every one of its indices, the index in the\n * source text that produced it. `map` has one extra trailing entry so a\n * half-open normalized span `[a, b)` maps to the source span `[map[a], map[b])`.\n *\n * This is what lets a fuzzy match be written back over exactly the bytes it\n * matched. Without it the only way to place a normalized match in the original\n * was to widen it to whole lines and re-emit the untouched remainder in\n * normalized form - which silently converted tabs, smart quotes and NFKC\n * lookalikes on any line an edit happened to land on.\n */\ninterface NormalizedWithMap {\n\ttext: string;\n\tmap: number[];\n}\n\n/** Combining marks attach to the preceding base character and normalize with it. */\nconst COMBINING_MARK = /\\p{Mn}/u;\n\n/**\n * NFKC, applied per grapheme cluster so each output character can be traced to\n * the cluster that produced it. Clustering keeps `e` + U+0301 composing into\n * `é` the way whole-string NFKC would, while `fi` still expands to `fi` with both\n * characters pointing at the single source ligature.\n */\nfunction nfkcWithMap(text: string): NormalizedWithMap {\n\tlet out = \"\";\n\tconst map: number[] = [];\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst start = i;\n\t\tconst base = String.fromCodePoint(text.codePointAt(i) as number);\n\t\ti += base.length;\n\t\tlet cluster = base;\n\t\twhile (i < text.length) {\n\t\t\tconst next = String.fromCodePoint(text.codePointAt(i) as number);\n\t\t\tif (!COMBINING_MARK.test(next)) break;\n\t\t\tcluster += next;\n\t\t\ti += next.length;\n\t\t}\n\t\tconst composed = cluster.normalize(\"NFKC\");\n\t\tfor (let k = 0; k < composed.length; k++) map.push(start);\n\t\tout += composed;\n\t}\n\tmap.push(text.length);\n\treturn { text: out, map };\n}\n\n/** CRLF and lone CR collapse to LF. */\nfunction toLfWithMap(text: string): NormalizedWithMap {\n\tlet out = \"\";\n\tconst map: number[] = [];\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif (ch === \"\\r\") {\n\t\t\tmap.push(i);\n\t\t\tout += \"\\n\";\n\t\t\tif (text[i + 1] === \"\\n\") i++;\n\t\t\tcontinue;\n\t\t}\n\t\tmap.push(i);\n\t\tout += ch;\n\t}\n\tmap.push(text.length);\n\treturn { text: out, map };\n}\n\n/**\n * The per-line whitespace pass: tabs widen to two spaces, interior runs of two\n * or more spaces collapse to one (leading indentation is left alone), and\n * trailing whitespace is dropped.\n */\nfunction normalizeLineWhitespaceWithMap(text: string): NormalizedWithMap {\n\tlet out = \"\";\n\tconst map: number[] = [];\n\tlet lineStart = 0;\n\twhile (lineStart <= text.length) {\n\t\tlet lineEnd = text.indexOf(\"\\n\", lineStart);\n\t\tconst hasNewline = lineEnd !== -1;\n\t\tif (!hasNewline) lineEnd = text.length;\n\n\t\t// Tabs first, so indentation is measured the way the old chain measured it.\n\t\tlet expanded = \"\";\n\t\tconst expandedMap: number[] = [];\n\t\tfor (let i = lineStart; i < lineEnd; i++) {\n\t\t\tif (text[i] === \"\\t\") {\n\t\t\t\texpanded += \" \";\n\t\t\t\texpandedMap.push(i, i);\n\t\t\t} else {\n\t\t\t\texpanded += text[i];\n\t\t\t\texpandedMap.push(i);\n\t\t\t}\n\t\t}\n\n\t\tconst leadingLength = (expanded.match(/^\\s*/)?.[0] ?? \"\").length;\n\t\tlet emitted = \"\";\n\t\tconst emittedMap: number[] = [];\n\t\tfor (let i = 0; i < expanded.length; i++) {\n\t\t\t// Collapse only runs that start past the indentation.\n\t\t\tif (i >= leadingLength && expanded[i] === \" \" && emitted.endsWith(\" \") && emitted.length > leadingLength) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\temitted += expanded[i];\n\t\t\temittedMap.push(expandedMap[i]);\n\t\t}\n\t\t// trimEnd\n\t\tlet end = emitted.length;\n\t\twhile (end > 0 && /\\s/.test(emitted[end - 1])) end--;\n\n\t\tout += emitted.slice(0, end);\n\t\tfor (let i = 0; i < end; i++) map.push(emittedMap[i]);\n\n\t\tif (hasNewline) {\n\t\t\tout += \"\\n\";\n\t\t\tmap.push(lineEnd);\n\t\t\tlineStart = lineEnd + 1;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\tmap.push(text.length);\n\treturn { text: out, map };\n}\n\n/** Compose `outer` (indices into `inner.text`) onto `inner`'s own source indices. */\nfunction composeMaps(outer: number[], inner: number[]): number[] {\n\treturn outer.map((i) => inner[i] ?? inner[inner.length - 1]);\n}\n\n/**\n * Normalize text for fuzzy matching, keeping an index back to the source for\n * every character produced. Same output text as `normalizeForFuzzyMatch`.\n */\nfunction normalizeForFuzzyMatchWithMap(text: string): NormalizedWithMap {\n\tconst nfkc = nfkcWithMap(text);\n\tconst lf = toLfWithMap(nfkc.text);\n\tconst lines = normalizeLineWhitespaceWithMap(lf.text);\n\t// The final substitutions are one-for-one, so they leave the map untouched.\n\tconst substituted = applyCharSubstitutions(lines.text);\n\treturn { text: substituted, map: composeMaps(composeMaps(lines.map, lf.map), nfkc.map) };\n}\n\n/** The one-for-one Unicode substitutions: quotes, dashes and exotic spaces. */\nfunction applyCharSubstitutions(text: string): string {\n\treturn text\n\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \");\n}\n\n/**\n * Normalize text for fuzzy matching. Applies progressive transformations:\n * - Normalize line endings to LF\n * - Strip trailing whitespace from each line\n * - Normalize tabs to spaces (2 spaces per tab)\n * - Collapse multiple spaces to single space\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nfunction normalizeForFuzzyMatch(text: string): string {\n\t// Check cache first\n\tconst cached = normalizeCache.get(text);\n\tif (cached !== undefined) return cached;\n\n\tconst normalized = text\n\t\t.normalize(\"NFKC\")\n\t\t// Normalize line endings to LF\n\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t.replace(/\\r/g, \"\\n\")\n\t\t// Strip trailing whitespace per line\n\t\t.split(\"\\n\")\n\t\t.map((line) => {\n\t\t\t// Normalize tabs to 2 spaces\n\t\t\tlet normalized = line.replace(/\\t/g, \" \");\n\t\t\t// Collapse multiple spaces to single space (but preserve leading indentation pattern)\n\t\t\t// Only collapse spaces that are NOT at the start of the line (indentation)\n\t\t\tconst leadingSpaces = normalized.match(/^(\\s*)/)?.[1] ?? \"\";\n\t\t\tconst rest = normalized.slice(leadingSpaces.length);\n\t\t\tnormalized = leadingSpaces + rest.replace(/ {2,}/g, \" \");\n\t\t\treturn normalized.trimEnd();\n\t\t})\n\t\t.join(\"\\n\")\n\t\t// Smart single quotes → '\n\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t// Smart double quotes → \"\n\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t// Various dashes/hyphens → -\n\t\t// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,\n\t\t// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus\n\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t// Special spaces → regular space\n\t\t// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,\n\t\t// U+205F medium math space, U+3000 ideographic space\n\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \");\n\n\t// Cache the result (with size limit)\n\tif (normalizeCache.size >= MAX_CACHE_SIZE) {\n\t\t// Remove oldest entry (first key)\n\t\tconst firstKey = normalizeCache.keys().next().value;\n\t\tif (firstKey !== undefined) {\n\t\t\tnormalizeCache.delete(firstKey);\n\t\t}\n\t}\n\tnormalizeCache.set(text, normalized);\n\n\treturn normalized;\n}\n\n/** Whether `index` in `text` is the first character of a line. */\nfunction isAtLineStart(text: string, index: number): boolean {\n\treturn index === 0 || text[index - 1] === \"\\n\";\n}\n\n/**\n * Indentation of `line`, with tabs widened the way `normalizeForFuzzyMatch`\n * widens them, so a tab-indented file and a two-space rendering of it compare\n * equal while genuinely different nesting levels do not.\n */\nfunction normalizedIndent(line: string): string {\n\treturn (line.match(/^[ \\t]*/)?.[0] ?? \"\").replace(/\\t/g, \" \");\n}\n\nexport interface Edit {\n\toldText: string;\n\tnewText: string;\n\t/**\n\t * When true, replace every occurrence of oldText instead of requiring it to\n\t * be unique. Default (false/undefined) keeps the uniqueness guardrail.\n\t */\n\treplaceAll?: boolean;\n}\n\ninterface MatchedEdit {\n\teditIndex: number;\n\tmatchIndex: number;\n\tmatchLength: number;\n\t/**\n\t * Text written over the span. Usually the edit's `newText` verbatim; for a\n\t * fuzzy match that covered only part of a line, the untouched remainder of\n\t * the first/last line rides along in normalized form (see `resolveFuzzySpan`).\n\t */\n\treplacement: string;\n}\n\n/** A match located in, and expressed in coordinates of, the original content. */\ninterface ResolvedSpan {\n\tmatchIndex: number;\n\tmatchLength: number;\n\treplacement: string;\n}\n\nexport interface AppliedEditsResult {\n\tbaseContent: string;\n\tnewContent: string;\n}\n\n/**\n * Locate one edit's `oldText` in `content`, always returning spans in\n * `content`'s own coordinates.\n *\n * Three tiers, tried in order: exact, fuzzy-normalized, then the\n * indentation-tolerant line-block fallback. `occurrences` counts the matches the\n * winning tier found, which is what the uniqueness guardrail tests; `spans` can\n * be shorter, because two fuzzy matches sharing a line are emitted as one\n * rewrite of that line.\n */\nfunction findEditSpans(\n\tcontent: string,\n\tedit: { oldText: string; newText: string },\n\tfuzzyIndex: () => NormalizedWithMap | null,\n): { spans: ResolvedSpan[]; occurrences: number; noopFuzzySpan?: ResolvedSpan } {\n\t// An oldText that opens with indentation is a statement about a whole line, so\n\t// it must not match mid-line inside a more deeply indented one - that lands the\n\t// edit in a different block entirely. An oldText that opens with a non-blank\n\t// character claims nothing about indentation, so every tier stays tolerant.\n\tconst anchored = /^[ \\t]/.test(edit.oldText);\n\n\t// Tier 1: exact. Already in original coordinates.\n\tconst exact = collectMatchIndices(content, edit.oldText).filter((i) => !anchored || isAtLineStart(content, i));\n\tif (exact.length > 0) {\n\t\treturn {\n\t\t\tspans: exact.map((matchIndex) => ({\n\t\t\t\tmatchIndex,\n\t\t\t\tmatchLength: edit.oldText.length,\n\t\t\t\treplacement: edit.newText,\n\t\t\t})),\n\t\t\toccurrences: exact.length,\n\t\t};\n\t}\n\n\t// Tier 2: fuzzy. Located in normalized space, then mapped straight back onto\n\t// the bytes it matched - nothing outside the match is rewritten.\n\tconst normalized = fuzzyIndex();\n\tif (normalized) {\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(edit.oldText);\n\t\tconst normalizedText = normalized.text;\n\t\tconst fuzzy = collectMatchIndices(normalizedText, fuzzyOldText).filter(\n\t\t\t(i) => !anchored || isAtLineStart(normalizedText, i),\n\t\t);\n\t\tconst spanAt = (index: number, replacement: string): ResolvedSpan => {\n\t\t\tconst start = normalized.map[index];\n\t\t\tconst end = normalized.map[index + fuzzyOldText.length];\n\t\t\treturn { matchIndex: start, matchLength: end - start, replacement };\n\t\t};\n\n\t\t/**\n\t\t * Build the replacement for a fuzzy match.\n\t\t *\n\t\t * newText is written as the model wrote it, with one exception: indentation.\n\t\t * A fuzzy match means oldText was not on disk byte-for-byte, so the whitespace\n\t\t * in it is the model's rendering of the line rather than a statement about the\n\t\t * file - and newText inherits that rendering. Writing it back re-indents lines\n\t\t * the edit never meant to touch, which in a tab-indented file means every\n\t\t * fuzzy edit silently converts tabs to spaces.\n\t\t *\n\t\t * So when newText's indentation says the same thing oldText's did, the file's\n\t\t * own indentation is kept. An edit that means to re-indent says so by giving\n\t\t * newText a different indentation from oldText, and that still applies.\n\t\t */\n\t\tconst replacementFor = (index: number): string => {\n\t\t\tconst start = normalized.map[index];\n\t\t\tconst end = normalized.map[index + fuzzyOldText.length];\n\t\t\tif (!isAtLineStart(content, start)) return edit.newText;\n\t\t\tconst originalLines = content.slice(start, end).split(\"\\n\");\n\t\t\tconst newLines = edit.newText.split(\"\\n\");\n\t\t\tconst oldLines = edit.oldText.split(\"\\n\");\n\t\t\tif (originalLines.length !== newLines.length || oldLines.length !== newLines.length) {\n\t\t\t\treturn edit.newText;\n\t\t\t}\n\t\t\treturn newLines\n\t\t\t\t.map((line, i) => {\n\t\t\t\t\tconst newIndent = line.match(/^[ \\t]*/)?.[0] ?? \"\";\n\t\t\t\t\tconst oldIndent = oldLines[i].match(/^[ \\t]*/)?.[0] ?? \"\";\n\t\t\t\t\tif (normalizedIndent(newIndent) !== normalizedIndent(oldIndent)) return line;\n\t\t\t\t\treturn (originalLines[i].match(/^[ \\t]*/)?.[0] ?? \"\") + line.slice(newIndent.length);\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t};\n\n\t\tif (fuzzy.length > 0) {\n\t\t\t// oldText did not match the file byte-for-byte, so the whitespace the model\n\t\t\t// used is its own rendering rather than a statement about the file. If\n\t\t\t// newText normalizes to the same thing, it asked for no change: leave the\n\t\t\t// span exactly as it is so the no-change error fires, instead of rewriting\n\t\t\t// the line's indentation to match the model's rendering of it.\n\t\t\tif (normalizeForFuzzyMatch(edit.newText) === fuzzyOldText) {\n\t\t\t\t// oldText did not match byte-for-byte, so its whitespace is the model's\n\t\t\t\t// rendering rather than a statement about the file, and newText asks for\n\t\t\t\t// nothing the matcher can see. Leave the bytes alone and report the span\n\t\t\t\t// so the caller can name the character the model failed to reproduce.\n\t\t\t\tconst untouched = fuzzy.map((index) => {\n\t\t\t\t\tconst span = spanAt(index, \"\");\n\t\t\t\t\treturn { ...span, replacement: content.slice(span.matchIndex, span.matchIndex + span.matchLength) };\n\t\t\t\t});\n\t\t\t\treturn { spans: untouched, occurrences: fuzzy.length, noopFuzzySpan: untouched[0] };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tspans: fuzzy.map((index) => spanAt(index, replacementFor(index))),\n\t\t\t\toccurrences: fuzzy.length,\n\t\t\t};\n\t\t}\n\t}\n\n\t// Tier 3: indentation-tolerant line blocks. Already in original coordinates.\n\tconst blocks = findLineBlockMatches(content, edit.oldText, anchored).map((span) => ({\n\t\tmatchIndex: span.matchIndex,\n\t\tmatchLength: span.matchLength,\n\t\treplacement: edit.newText,\n\t}));\n\treturn { spans: blocks, occurrences: blocks.length };\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Collect the start index of every non-overlapping occurrence of needle in\n * haystack. The needle must already be in the same space as haystack (raw for\n * exact matches, fuzzy-normalized when haystack is fuzzy-normalized).\n */\nfunction collectMatchIndices(haystack: string, needle: string): number[] {\n\tconst indices: number[] = [];\n\tif (needle.length === 0) return indices;\n\tlet from = 0;\n\twhile (true) {\n\t\tconst idx = haystack.indexOf(needle, from);\n\t\tif (idx === -1) break;\n\t\tindices.push(idx);\n\t\tfrom = idx + needle.length; // non-overlapping\n\t}\n\treturn indices;\n}\n\n/**\n * Per-line normalization for indentation-tolerant block matching. Applies the\n * same Unicode/space normalization as fuzzy matching, then strips all leading\n * and trailing whitespace so indentation differences are ignored entirely.\n */\nfunction blockNormalizeLine(line: string): string {\n\treturn normalizeForFuzzyMatch(line).trim();\n}\n\ninterface LineBlockMatch {\n\tmatchIndex: number;\n\tmatchLength: number;\n}\n\n/**\n * Indentation-tolerant fallback matcher. Compares oldText against content line\n * by line, ignoring each line's leading/trailing whitespace (and Unicode\n * formatting). Returns character spans in `content` for every block whose\n * trimmed lines equal the trimmed oldText lines. Replacement still happens in\n * the original content space, so surrounding formatting is preserved.\n */\nfunction findLineBlockMatches(content: string, oldText: string, anchored = false): LineBlockMatch[] {\n\tconst hadTrailingNewline = oldText.endsWith(\"\\n\");\n\tconst oldLines = oldText.split(\"\\n\");\n\tif (hadTrailingNewline) oldLines.pop();\n\tif (oldLines.length === 0) return [];\n\tconst trimmedOld = oldLines.map(blockNormalizeLine);\n\n\tconst contentLines = content.split(\"\\n\");\n\tconst k = trimmedOld.length;\n\tif (k > contentLines.length) return [];\n\n\t// Char offset of each line start within content.\n\tconst offsets = new Array<number>(contentLines.length);\n\tlet acc = 0;\n\tfor (let i = 0; i < contentLines.length; i++) {\n\t\toffsets[i] = acc;\n\t\tacc += contentLines[i].length + 1; // + newline\n\t}\n\n\tconst matches: LineBlockMatch[] = [];\n\tfor (let i = 0; i + k <= contentLines.length; i++) {\n\t\tlet ok = true;\n\t\tfor (let j = 0; j < k; j++) {\n\t\t\tif (blockNormalizeLine(contentLines[i + j]) !== trimmedOld[j]) {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// Tier 3 ignores indentation by design. When oldText stated its own\n\t\t\t// indentation, honour that statement rather than matching any nesting level.\n\t\t\tif (anchored && normalizedIndent(contentLines[i + j]) !== normalizedIndent(oldLines[j])) {\n\t\t\t\tok = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!ok) continue;\n\t\tconst matchIndex = offsets[i];\n\t\tlet matchLength = 0;\n\t\tfor (let j = 0; j < k; j++) matchLength += contentLines[i + j].length + (j < k - 1 ? 1 : 0);\n\t\t// Include the trailing newline when oldText carried one and a line follows the block.\n\t\tif (hadTrailingNewline && i + k < contentLines.length) matchLength += 1;\n\t\tmatches.push({ matchIndex, matchLength });\n\t}\n\treturn matches;\n}\n\nfunction getNotFoundError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`,\n\t);\n}\n\nfunction getDuplicateError(path: string, editIndex: number, totalEdits: number, occurrences: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`,\n\t);\n}\n\nfunction getEmptyOldTextError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(`oldText must not be empty in ${path}.`);\n\t}\n\treturn new Error(`edits[${editIndex}].oldText must not be empty in ${path}.`);\n}\n\n/**\n * Characters the fuzzy matcher erases. When an edit matched only fuzzily and its\n * newText normalizes to the same text, one of these is why: the file holds a\n * character the model reproduced as its plain-ASCII lookalike, so the change it\n * asked for is invisible to the matcher and can never be applied by retrying the\n * same text. Naming the character is the whole recovery - resend oldText with it.\n */\nconst NORMALIZED_AWAY_NAMES = new Map<number, string>([\n\t[0x0009, \"TAB\"],\n\t[0x00a0, \"NO-BREAK SPACE\"],\n\t[0x2002, \"EN SPACE\"],\n\t[0x2003, \"EM SPACE\"],\n\t[0x2009, \"THIN SPACE\"],\n\t[0x200a, \"HAIR SPACE\"],\n\t[0x2010, \"HYPHEN\"],\n\t[0x2011, \"NON-BREAKING HYPHEN\"],\n\t[0x2012, \"FIGURE DASH\"],\n\t[0x2013, \"EN DASH\"],\n\t[0x2014, \"EM DASH\"],\n\t[0x2015, \"HORIZONTAL BAR\"],\n\t[0x2018, \"LEFT SINGLE QUOTATION MARK\"],\n\t[0x2019, \"RIGHT SINGLE QUOTATION MARK\"],\n\t[0x201a, \"SINGLE LOW-9 QUOTATION MARK\"],\n\t[0x201b, \"SINGLE HIGH-REVERSED-9 QUOTATION MARK\"],\n\t[0x201c, \"LEFT DOUBLE QUOTATION MARK\"],\n\t[0x201d, \"RIGHT DOUBLE QUOTATION MARK\"],\n\t[0x201e, \"DOUBLE LOW-9 QUOTATION MARK\"],\n\t[0x201f, \"DOUBLE HIGH-REVERSED-9 QUOTATION MARK\"],\n\t[0x202f, \"NARROW NO-BREAK SPACE\"],\n\t[0x205f, \"MEDIUM MATHEMATICAL SPACE\"],\n\t[0x2212, \"MINUS SIGN\"],\n\t[0x3000, \"IDEOGRAPHIC SPACE\"],\n]);\n\nfunction formatCodePoint(ch: string): string {\n\tconst cp = ch.codePointAt(0) ?? 0;\n\tconst hex = `U+${cp.toString(16).toUpperCase().padStart(4, \"0\")}`;\n\tconst name = NORMALIZED_AWAY_NAMES.get(cp);\n\tif (name) return `${hex} ${name}`;\n\t// NFKC-only difference (ligature, full-width form, superscript, ...).\n\treturn `${hex} (normalizes to ${JSON.stringify(ch.normalize(\"NFKC\"))})`;\n}\n\n/** Cap on how many offending characters one error names before summarising. */\nconst MAX_REPORTED_CHARS = 5;\n\n/**\n * Every character in `text` that fuzzy normalization would not leave alone.\n *\n * All of them are listed rather than just the first: the match span is widened to\n * whole lines, so the first offender is often a leading tab the model never put\n * in its oldText, while the character it actually needs sits further along the\n * line. Naming one of them and guessing wrong is worse than naming them all.\n */\nfunction findNormalizedAwayChars(text: string): Array<{ ch: string; index: number }> {\n\tconst found: Array<{ ch: string; index: number }> = [];\n\tlet index = 0;\n\tfor (const ch of text) {\n\t\tconst cp = ch.codePointAt(0) ?? 0;\n\t\tif (NORMALIZED_AWAY_NAMES.has(cp) || ch.normalize(\"NFKC\") !== ch) {\n\t\t\tfound.push({ ch, index });\n\t\t}\n\t\tindex += ch.length;\n\t}\n\treturn found;\n}\n\n/** 1-indexed line and column of `offset` within `content`. */\nfunction lineAndColumn(content: string, offset: number): { line: number; column: number } {\n\tconst before = content.slice(0, offset);\n\tconst line = before.split(\"\\n\").length;\n\tconst column = offset - (before.lastIndexOf(\"\\n\") + 1) + 1;\n\treturn { line, column };\n}\n\n/**\n * The edit matched, but only after normalization erased the very difference it\n * asked for. Retrying the same oldText can never succeed, so say which character\n * is actually on disk and where.\n */\nfunction getFuzzyNoopError(\n\tpath: string,\n\teditIndex: number,\n\ttotalEdits: number,\n\tcontent: string,\n\tspan: ResolvedSpan,\n): Error {\n\tconst matched = content.slice(span.matchIndex, span.matchIndex + span.matchLength);\n\tconst which = totalEdits === 1 ? \"The edit\" : `edits[${editIndex}]`;\n\tconst offenders = findNormalizedAwayChars(matched);\n\tlet detail: string;\n\tif (offenders.length > 0) {\n\t\tconst shown = offenders.slice(0, MAX_REPORTED_CHARS).map((o) => {\n\t\t\tconst { line, column } = lineAndColumn(content, span.matchIndex + o.index);\n\t\t\treturn `${formatCodePoint(o.ch)} at line ${line}, column ${column}`;\n\t\t});\n\t\tconst more = offenders.length - shown.length;\n\t\tdetail =\n\t\t\t`the text it matched in ${path} contains ${shown.join(\"; \")}` +\n\t\t\t`${more > 0 ? `; and ${more} more` : \"\"}, which your oldText spelled as plain-ASCII lookalikes. ` +\n\t\t\t`Send oldText containing those exact characters and the replacement will apply.`;\n\t} else {\n\t\tdetail =\n\t\t\t`oldText matched ${path} only after whitespace normalization, and newText normalizes to the same text, ` +\n\t\t\t`so nothing would change. Send oldText exactly as the file spells it.`;\n\t}\n\treturn new Error(`No changes made to ${path}. ${which} asked for a change that is invisible to matching: ${detail}`);\n}\n\nfunction getNoChangeError(path: string, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,\n\t\t);\n\t}\n\treturn new Error(`No changes made to ${path}. The replacements produced identical content.`);\n}\n\n/**\n * Apply one or more exact-text replacements to LF-normalized content.\n *\n * All edits are matched against the same original content. Replacements are\n * then applied in reverse order so offsets remain stable.\n *\n * Every match, however loosely it was found, is resolved back to a span of the\n * *original* content before anything is written. A fuzzy or indentation-tolerant\n * match therefore rewrites only the lines it landed on: the rest of the file\n * keeps its exact bytes, and the diff callers render from `baseContent` is the\n * real change on disk rather than a normalized-vs-normalized view of it.\n */\nexport function applyEditsToNormalizedContent(\n\tnormalizedContent: string,\n\tedits: Edit[],\n\tpath: string,\n): AppliedEditsResult {\n\tconst normalizedEdits = edits.map((edit) => ({\n\t\toldText: normalizeToLF(edit.oldText),\n\t\tnewText: normalizeToLF(edit.newText),\n\t\treplaceAll: edit.replaceAll === true,\n\t}));\n\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tif (normalizedEdits[i].oldText.length === 0) {\n\t\t\tthrow getEmptyOldTextError(path, i, normalizedEdits.length);\n\t\t}\n\t}\n\n\tconst baseContent = normalizedContent;\n\n\t// Needed only if some edit reaches the fuzzy tier, and identical for every\n\t// edit, so build it at most once. A map that does not line up with its own\n\t// text would put spans in the wrong place, so the tier is skipped rather than\n\t// trusted if that ever fails to hold.\n\tlet normalizedCache: NormalizedWithMap | null | undefined;\n\tconst fuzzyIndex = () => {\n\t\tif (normalizedCache === undefined) {\n\t\t\tconst built = normalizeForFuzzyMatchWithMap(baseContent);\n\t\t\tnormalizedCache = built.map.length === built.text.length + 1 ? built : null;\n\t\t}\n\t\treturn normalizedCache;\n\t};\n\n\tconst matchedEdits: MatchedEdit[] = [];\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tconst edit = normalizedEdits[i];\n\t\tconst { spans, occurrences, noopFuzzySpan } = findEditSpans(baseContent, edit, fuzzyIndex);\n\t\tif (spans.length === 0) {\n\t\t\tthrow getNotFoundError(path, i, normalizedEdits.length);\n\t\t}\n\t\t// Raised per edit, not once for the whole call: a no-op hidden among edits\n\t\t// that do change bytes used to be swallowed and reported as a success.\n\t\tif (noopFuzzySpan) {\n\t\t\tthrow getFuzzyNoopError(path, i, normalizedEdits.length, baseContent, noopFuzzySpan);\n\t\t}\n\n\t\tif (edit.replaceAll) {\n\t\t\t// Replace every occurrence so the shared reverse-order applier rewrites them all.\n\t\t\tfor (const span of spans) {\n\t\t\t\tmatchedEdits.push({\n\t\t\t\t\teditIndex: i,\n\t\t\t\t\tmatchIndex: span.matchIndex,\n\t\t\t\t\tmatchLength: span.matchLength,\n\t\t\t\t\treplacement: span.replacement,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (occurrences > 1) {\n\t\t\tthrow getDuplicateError(path, i, normalizedEdits.length, occurrences);\n\t\t}\n\n\t\tmatchedEdits.push({\n\t\t\teditIndex: i,\n\t\t\tmatchIndex: spans[0].matchIndex,\n\t\t\tmatchLength: spans[0].matchLength,\n\t\t\treplacement: spans[0].replacement,\n\t\t});\n\t}\n\n\tmatchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);\n\tfor (let i = 1; i < matchedEdits.length; i++) {\n\t\tconst previous = matchedEdits[i - 1];\n\t\tconst current = matchedEdits[i];\n\t\tif (previous.matchIndex + previous.matchLength > current.matchIndex) {\n\t\t\tthrow new Error(\n\t\t\t\t`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tlet newContent = baseContent;\n\tfor (let i = matchedEdits.length - 1; i >= 0; i--) {\n\t\tconst edit = matchedEdits[i];\n\t\tnewContent =\n\t\t\tnewContent.substring(0, edit.matchIndex) +\n\t\t\tedit.replacement +\n\t\t\tnewContent.substring(edit.matchIndex + edit.matchLength);\n\t}\n\n\tif (baseContent === newContent) {\n\t\tthrow getNoChangeError(path, normalizedEdits.length);\n\t}\n\n\treturn { baseContent, newContent };\n}\n\n/**\n * Generate a unified diff string with line numbers and context.\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\tlet firstChangedLine: number | undefined;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Capture the first changed line (in the new file)\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLineNum;\n\t\t\t}\n\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\t\t\tconst hasLeadingChange = lastWasChange;\n\t\t\tconst hasTrailingChange = nextPartIsChange;\n\n\t\t\tif (hasLeadingChange && hasTrailingChange) {\n\t\t\t\tif (raw.length <= contextLines * 2) {\n\t\t\t\t\tfor (const line of raw) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst leadingLines = raw.slice(0, contextLines);\n\t\t\t\t\tconst trailingLines = raw.slice(raw.length - contextLines);\n\t\t\t\t\tconst skippedLines = raw.length - leadingLines.length - trailingLines.length;\n\n\t\t\t\t\tfor (const line of leadingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\n\t\t\t\t\tfor (const line of trailingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasLeadingChange) {\n\t\t\t\tconst shownLines = raw.slice(0, contextLines);\n\t\t\t\tconst skippedLines = raw.length - shownLines.length;\n\n\t\t\t\tfor (const line of shownLines) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\t\t\t} else if (hasTrailingChange) {\n\t\t\t\tconst skippedLines = Math.max(0, raw.length - contextLines);\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\n\t\t\t\tfor (const line of raw.slice(skippedLines)) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn { diff: output.join(\"\\n\"), firstChangedLine };\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for one or more edit operations without applying them.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditsDiff(\n\tpath: string,\n\tedits: Edit[],\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\treturn { error: `Could not edit file: ${path}. ${errorMessage}.` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
|
|
@@ -38,8 +38,19 @@ export declare function rangesOverlap(a: ReadRange, b: ReadRange): boolean;
|
|
|
38
38
|
export declare const DEDUP_POINTER_PREFIX = "[Already in context:";
|
|
39
39
|
/** Whether a tool-result text is an at-call dedup pointer. */
|
|
40
40
|
export declare function isDedupPointerText(text: string): boolean;
|
|
41
|
+
/** Remember the stamp of the file a read delivered. */
|
|
42
|
+
export declare function recordReadStamp(callId: string, stamp: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the file a read delivered still stamps the same. False when no stamp
|
|
45
|
+
* was recorded, so an unprovable case re-reads rather than asserting freshness.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readStampMatches(callId: string, stamp: string): boolean;
|
|
48
|
+
/** Test seam: drop all recorded stamps. */
|
|
49
|
+
export declare function clearReadStamps(): void;
|
|
41
50
|
/** A covering earlier read, described for the pointer message. */
|
|
42
51
|
export interface CoveringRead {
|
|
52
|
+
/** Tool call id of the read being pointed at, used to check its stamp. */
|
|
53
|
+
callId: string;
|
|
43
54
|
/** The path as the earlier read spelled it (for a friendly pointer). */
|
|
44
55
|
display: string;
|
|
45
56
|
/** Delivered range start (1-indexed line). */
|
|
@@ -71,6 +82,8 @@ export interface FindCoveringReadOptions {
|
|
|
71
82
|
* the candidate and the supersession side.
|
|
72
83
|
*/
|
|
73
84
|
export declare function findCoveringRead(entries: readonly unknown[], opts: FindCoveringReadOptions): CoveringRead | null;
|
|
85
|
+
/** The parts of a covering read the pointer message renders. */
|
|
86
|
+
export type CoveringReadDisplay = Pick<CoveringRead, "display" | "start" | "end">;
|
|
74
87
|
/** Build the pointer text returned in place of a re-fetch. */
|
|
75
|
-
export declare function buildDedupPointerText(covering:
|
|
88
|
+
export declare function buildDedupPointerText(covering: CoveringReadDisplay): string;
|
|
76
89
|
//# sourceMappingURL=read-dedup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-dedup.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read-dedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,mFAAmF;AACnF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,EAAE,SAAuD,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,SAAS,CAStF;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,OAAO,CAEjE;AAOD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC5B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACvC,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,cAAc,EAAE,SAAS,CAAC;IAC1B,mFAAmF;IACnF,aAAa,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CACzC;AAoED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,uBAAuB,GAAG,YAAY,GAAG,IAAI,CA8FhH;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAQpE","sourcesContent":["/**\n * Read de-duplication primitives.\n *\n * Two mechanisms share this module:\n *\n * - The post-hoc context GC (`context-gc.ts`), which stubs out a `read` result\n * once a later overlapping read or an edit/write has superseded it.\n * - The at-call-time guard in the `read` tool, which short-circuits a read whose\n * requested range is *already fully covered* by an earlier, still-live read in\n * the current session, returning a pointer instead of re-fetching the file.\n *\n * Both reason about half-open line ranges `[start, end)` (end exclusive), so the\n * range math lives here and cannot drift between them. The guard additionally\n * needs to (a) recognise its own pointer results so they are never treated as\n * content-bearing reads, and (b) walk the session branch to find a covering\n * read — both of which are defined here to keep the tool file lean.\n */\n\n/** Half-open line interval `[start, end)` a read call covers; end is exclusive. */\nexport interface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/** A read with no offset/limit covers the whole file (open-ended). */\nexport const WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nexport function readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nexport function rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\n/** Whether `outer` fully contains `inner` (every line of inner lies within outer). */\nfunction rangeContains(outer: ReadRange, inner: ReadRange): boolean {\n\treturn outer.start <= inner.start && outer.end >= inner.end;\n}\n\n/**\n * Marker prefix for the at-call dedup pointer. Kept stable so the context GC can\n * recognise a pointer result and exclude it from supersession bookkeeping (a\n * pointer fetched no content, so it must not stub the read it points at).\n */\nexport const DEDUP_POINTER_PREFIX = \"[Already in context:\";\n\n/** Whether a tool-result text is an at-call dedup pointer. */\nexport function isDedupPointerText(text: string): boolean {\n\treturn text.trimStart().startsWith(DEDUP_POINTER_PREFIX);\n}\n\n/** A covering earlier read, described for the pointer message. */\nexport interface CoveringRead {\n\t/** The path as the earlier read spelled it (for a friendly pointer). */\n\tdisplay: string;\n\t/** Delivered range start (1-indexed line). */\n\tstart: number;\n\t/** Delivered range end (exclusive; Infinity for a whole-file read). */\n\tend: number;\n}\n\nexport interface FindCoveringReadOptions {\n\t/** Resolved absolute path of the current read. */\n\tresolvedPath: string;\n\t/** Range the current read is asking for. */\n\trequestedRange: ReadRange;\n\t/** Tool call id of the current read, excluded from candidate/supersession sets. */\n\tcurrentCallId: string;\n\t/** Resolve a raw read-arg path the same way the current read resolved its path. */\n\tresolvePath: (rawPath: string) => string;\n}\n\ninterface ContentBlock {\n\ttype?: string;\n\tid?: string;\n\tname?: string;\n\targuments?: Record<string, unknown>;\n\ttext?: string;\n}\n\ninterface MessageLike {\n\trole?: string;\n\tcontent?: ContentBlock[];\n\ttoolCallId?: string;\n\ttoolName?: string;\n\tisError?: boolean;\n}\n\nconst READ_TOOL = \"read\";\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** A session compaction entry — the boundary that trims the live context. */\nfunction isCompactionEntry(entry: unknown): boolean {\n\treturn !!entry && typeof entry === \"object\" && (entry as { type?: unknown }).type === \"compaction\";\n}\n\n/** Accept either raw `AgentMessage`s or session entries wrapping `.message`. */\nfunction toMessage(entry: unknown): MessageLike | null {\n\tif (!entry || typeof entry !== \"object\") return null;\n\tconst e = entry as { message?: unknown; role?: unknown };\n\tif (e.message && typeof e.message === \"object\") return e.message as MessageLike;\n\tif (typeof e.role === \"string\") return e as MessageLike;\n\treturn null;\n}\n\nfunction resultText(m: MessageLike): string {\n\treturn (m.content ?? [])\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text ?? \"\")\n\t\t.join(\"\");\n}\n\n/**\n * Recover the range a read result actually *delivered* from its text.\n *\n * A cap-truncated read announces `[Showing lines A-B of N ...]`, so it delivered\n * only `[A, B+1)` even though its args declared a wider range. A read whose first\n * line alone exceeded the byte cap delivered nothing. Any other (untruncated)\n * read delivered its full declared range. The user-`limit` early-stop notice\n * (`[N more lines in file ...]`) is *not* a cap truncation — the declared range\n * was delivered in full — so it falls through to `declared`.\n */\nfunction deliveredRange(text: string, declared: ReadRange): ReadRange {\n\t// The truncation notice is always the trailing `\\n\\n[Showing lines A-B of N ...]`\n\t// clause the read tool appends. Anchor to the end so a `[Showing lines ...]`\n\t// string that merely appears *inside* the file's content can't spoof it.\n\tconst showing = text.match(/\\n\\n\\[Showing lines (\\d+)-(\\d+) of \\d+[^\\]]*\\]\\s*$/);\n\tif (showing) {\n\t\tconst a = Number(showing[1]);\n\t\tconst b = Number(showing[2]);\n\t\tif (Number.isFinite(a) && Number.isFinite(b) && b >= a) return { start: a, end: b + 1 };\n\t}\n\t// First line alone exceeded the byte limit: the whole result *is* that notice,\n\t// so it must start the text. Nothing usable was delivered.\n\tif (/^\\[Line \\d+ is .+ exceeds .+ limit\\./.test(text)) return { start: 1, end: 1 };\n\treturn declared;\n}\n\n/**\n * Find the latest earlier read that (a) is for the same resolved path, (b)\n * actually delivered a range containing the requested range, and (c) is still\n * live in the outgoing context — i.e. the post-hoc GC will not have stubbed it,\n * because no later edit/write and no later overlapping content read supersede\n * it. Returns that read's delivered range for the pointer, or null when the\n * current read must actually run.\n *\n * Deliberately conservative: a truncated earlier read only covers what it\n * delivered, a whole-file read must have been delivered untruncated to count as\n * covering, and any pointer results (which fetched nothing) are ignored on both\n * the candidate and the supersession side.\n */\nexport function findCoveringRead(entries: readonly unknown[], opts: FindCoveringReadOptions): CoveringRead | null {\n\t// `declared` mirrors the range the GC uses for supersession (straight from the\n\t// call args); `delivered` is what the result text shows was actually returned,\n\t// used for coverage. The current call has no result yet, so it never appears.\n\tinterface PriorRead {\n\t\tindex: number;\n\t\tdeclared: ReadRange;\n\t\tdelivered: ReadRange;\n\t\tdisplay: string;\n\t}\n\n\t// Resolve each distinct raw path once — the resolver may hit the filesystem.\n\tconst resolveCache = new Map<string, string>();\n\tconst resolvePath = (raw: string): string => {\n\t\tconst hit = resolveCache.get(raw);\n\t\tif (hit !== undefined) return hit;\n\t\tconst resolved = opts.resolvePath(raw);\n\t\tresolveCache.set(raw, resolved);\n\t\treturn resolved;\n\t};\n\n\t// Single ordered pass. `readCall`/`mutateCallPath` map a call id to its path as\n\t// the call is seen (a toolCall always precedes its result); `reads` collects\n\t// the target path's reads and `lastMutateIndex` its last mutate. `order` gives\n\t// live-context position for the supersession/mutate comparisons.\n\tconst readCall = new Map<string, { resolved: string; display: string; declared: ReadRange }>();\n\tconst mutateCallPath = new Map<string, string>();\n\tconst reads: PriorRead[] = [];\n\tlet lastMutateIndex = -1;\n\tlet order = 0;\n\n\tfor (const entry of entries) {\n\t\t// Compaction boundary: everything before it is replaced by a summary in the\n\t\t// live context, so drop the state accumulated so far. Conservative — the\n\t\t// kept tail before the boundary is dropped too — which can only miss a\n\t\t// dedup, never point at content that is no longer in context.\n\t\tif (isCompactionEntry(entry)) {\n\t\t\treadCall.clear();\n\t\t\tmutateCallPath.clear();\n\t\t\treads.length = 0;\n\t\t\tlastMutateIndex = -1;\n\t\t\torder = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tconst m = toMessage(entry);\n\t\tif (!m) continue;\n\t\tconst i = order++;\n\t\tif (m.role === \"assistant\" && Array.isArray(m.content)) {\n\t\t\tfor (const b of m.content) {\n\t\t\t\tif (b.type !== \"toolCall\" || !b.id) continue;\n\t\t\t\tconst raw = b.arguments?.path;\n\t\t\t\tif (typeof raw !== \"string\" || raw.length === 0) continue;\n\t\t\t\tconst resolved = resolvePath(raw);\n\t\t\t\tif (b.name === READ_TOOL) {\n\t\t\t\t\treadCall.set(b.id, { resolved, display: raw, declared: readRangeFromArgs(b.arguments) });\n\t\t\t\t} else if (b.name && MUTATE_TOOLS.has(b.name)) {\n\t\t\t\t\tmutateCallPath.set(b.id, resolved);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (m.role === \"toolResult\" && !m.isError && m.toolCallId) {\n\t\t\tif (m.toolName === READ_TOOL) {\n\t\t\t\tif (m.toolCallId === opts.currentCallId) continue;\n\t\t\t\tconst info = readCall.get(m.toolCallId);\n\t\t\t\tif (!info || info.resolved !== opts.resolvedPath) continue;\n\t\t\t\tconst text = resultText(m);\n\t\t\t\t// A pointer fetched nothing: the GC excludes it from supersession, so we\n\t\t\t\t// must too (both as a candidate and as a superseder).\n\t\t\t\tif (isDedupPointerText(text)) continue;\n\t\t\t\treads.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\tdeclared: info.declared,\n\t\t\t\t\tdelivered: deliveredRange(text, info.declared),\n\t\t\t\t\tdisplay: info.display,\n\t\t\t\t});\n\t\t\t} else if (m.toolName && MUTATE_TOOLS.has(m.toolName)) {\n\t\t\t\tif (mutateCallPath.get(m.toolCallId) === opts.resolvedPath) lastMutateIndex = i;\n\t\t\t}\n\t\t}\n\t}\n\n\t// A read survives the GC iff no later mutate and no later read overlaps its\n\t// *declared* range — exactly the GC's own test — so predict it the same way.\n\tconst survivesGc = (r: PriorRead): boolean =>\n\t\tlastMutateIndex <= r.index && !reads.some((o) => o.index > r.index && rangesOverlap(o.declared, r.declared));\n\n\tlet best: PriorRead | null = null;\n\tfor (const r of reads) {\n\t\t// Coverage uses the *delivered* range: a truncated read only holds what it returned.\n\t\tif (!rangeContains(r.delivered, opts.requestedRange)) continue;\n\t\tif (!survivesGc(r)) continue;\n\t\tif (!best || r.index > best.index) best = r;\n\t}\n\tif (!best) return null;\n\treturn { display: best.display, start: best.delivered.start, end: best.delivered.end };\n}\n\n/** Build the pointer text returned in place of a re-fetch. */\nexport function buildDedupPointerText(covering: CoveringRead): string {\n\tconst where =\n\t\tcovering.end === Number.POSITIVE_INFINITY\n\t\t\t? \"the entire file\"\n\t\t\t: covering.end - 1 > covering.start\n\t\t\t\t? `lines ${covering.start}-${covering.end - 1}`\n\t\t\t\t: `line ${covering.start}`;\n\treturn `${DEDUP_POINTER_PREFIX} ${covering.display} (${where}) was already read earlier in this session and has not changed since. Not re-fetched to save tokens — pass a different offset/limit, or edit the file, if you need other or newer content.]`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read-dedup.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read-dedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,mFAAmF;AACnF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,EAAE,SAAuD,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,SAAS,CAStF;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,OAAO,CAEjE;AAOD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAkBD,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMnE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGvE;AAED,2CAA2C;AAC3C,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC5B,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACvC,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,cAAc,EAAE,SAAS,CAAC;IAC1B,mFAAmF;IACnF,aAAa,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CACzC;AAoED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,uBAAuB,GAAG,YAAY,GAAG,IAAI,CA8GhH;AAED,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC;AAElF,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,MAAM,CAQ3E","sourcesContent":["/**\n * Read de-duplication primitives.\n *\n * Two mechanisms share this module:\n *\n * - The post-hoc context GC (`context-gc.ts`), which stubs out a `read` result\n * once a later overlapping read or an edit/write has superseded it.\n * - The at-call-time guard in the `read` tool, which short-circuits a read whose\n * requested range is *already fully covered* by an earlier, still-live read in\n * the current session, returning a pointer instead of re-fetching the file.\n *\n * Both reason about half-open line ranges `[start, end)` (end exclusive), so the\n * range math lives here and cannot drift between them. The guard additionally\n * needs to (a) recognise its own pointer results so they are never treated as\n * content-bearing reads, and (b) walk the session branch to find a covering\n * read — both of which are defined here to keep the tool file lean.\n */\n\n/** Half-open line interval `[start, end)` a read call covers; end is exclusive. */\nexport interface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/** A read with no offset/limit covers the whole file (open-ended). */\nexport const WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nexport function readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nexport function rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\n/** Whether `outer` fully contains `inner` (every line of inner lies within outer). */\nfunction rangeContains(outer: ReadRange, inner: ReadRange): boolean {\n\treturn outer.start <= inner.start && outer.end >= inner.end;\n}\n\n/**\n * Marker prefix for the at-call dedup pointer. Kept stable so the context GC can\n * recognise a pointer result and exclude it from supersession bookkeeping (a\n * pointer fetched no content, so it must not stub the read it points at).\n */\nexport const DEDUP_POINTER_PREFIX = \"[Already in context:\";\n\n/** Whether a tool-result text is an at-call dedup pointer. */\nexport function isDedupPointerText(text: string): boolean {\n\treturn text.trimStart().startsWith(DEDUP_POINTER_PREFIX);\n}\n\n/**\n * Content stamps for reads that have already been delivered.\n *\n * The dedup pointer tells the model the file \"has not changed since\" the read it\n * names. Nothing in the transcript can establish that: an editor, a formatter, a\n * branch switch or a second agent can rewrite the file between two reads without\n * leaving a trace in it. So each delivered read records a stamp of the file it\n * saw, and the pointer is only served when the file still stamps the same.\n *\n * Keyed by tool call id rather than by path, so two sessions sharing a process\n * cannot overwrite each other's observations. Bounded, and a missing entry\n * counts as \"cannot prove it is unchanged\" - the read then simply runs.\n */\nconst MAX_TRACKED_READS = 500;\nconst stampByCallId = new Map<string, string>();\n\n/** Remember the stamp of the file a read delivered. */\nexport function recordReadStamp(callId: string, stamp: string): void {\n\tif (stampByCallId.size >= MAX_TRACKED_READS) {\n\t\tconst oldest = stampByCallId.keys().next().value;\n\t\tif (oldest !== undefined) stampByCallId.delete(oldest);\n\t}\n\tstampByCallId.set(callId, stamp);\n}\n\n/**\n * Whether the file a read delivered still stamps the same. False when no stamp\n * was recorded, so an unprovable case re-reads rather than asserting freshness.\n */\nexport function readStampMatches(callId: string, stamp: string): boolean {\n\tconst recorded = stampByCallId.get(callId);\n\treturn recorded !== undefined && recorded === stamp;\n}\n\n/** Test seam: drop all recorded stamps. */\nexport function clearReadStamps(): void {\n\tstampByCallId.clear();\n}\n\n/** A covering earlier read, described for the pointer message. */\nexport interface CoveringRead {\n\t/** Tool call id of the read being pointed at, used to check its stamp. */\n\tcallId: string;\n\t/** The path as the earlier read spelled it (for a friendly pointer). */\n\tdisplay: string;\n\t/** Delivered range start (1-indexed line). */\n\tstart: number;\n\t/** Delivered range end (exclusive; Infinity for a whole-file read). */\n\tend: number;\n}\n\nexport interface FindCoveringReadOptions {\n\t/** Resolved absolute path of the current read. */\n\tresolvedPath: string;\n\t/** Range the current read is asking for. */\n\trequestedRange: ReadRange;\n\t/** Tool call id of the current read, excluded from candidate/supersession sets. */\n\tcurrentCallId: string;\n\t/** Resolve a raw read-arg path the same way the current read resolved its path. */\n\tresolvePath: (rawPath: string) => string;\n}\n\ninterface ContentBlock {\n\ttype?: string;\n\tid?: string;\n\tname?: string;\n\targuments?: Record<string, unknown>;\n\ttext?: string;\n}\n\ninterface MessageLike {\n\trole?: string;\n\tcontent?: ContentBlock[];\n\ttoolCallId?: string;\n\ttoolName?: string;\n\tisError?: boolean;\n}\n\nconst READ_TOOL = \"read\";\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** A session compaction entry — the boundary that trims the live context. */\nfunction isCompactionEntry(entry: unknown): boolean {\n\treturn !!entry && typeof entry === \"object\" && (entry as { type?: unknown }).type === \"compaction\";\n}\n\n/** Accept either raw `AgentMessage`s or session entries wrapping `.message`. */\nfunction toMessage(entry: unknown): MessageLike | null {\n\tif (!entry || typeof entry !== \"object\") return null;\n\tconst e = entry as { message?: unknown; role?: unknown };\n\tif (e.message && typeof e.message === \"object\") return e.message as MessageLike;\n\tif (typeof e.role === \"string\") return e as MessageLike;\n\treturn null;\n}\n\nfunction resultText(m: MessageLike): string {\n\treturn (m.content ?? [])\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text ?? \"\")\n\t\t.join(\"\");\n}\n\n/**\n * Recover the range a read result actually *delivered* from its text.\n *\n * A cap-truncated read announces `[Showing lines A-B of N ...]`, so it delivered\n * only `[A, B+1)` even though its args declared a wider range. A read whose first\n * line alone exceeded the byte cap delivered nothing. Any other (untruncated)\n * read delivered its full declared range. The user-`limit` early-stop notice\n * (`[N more lines in file ...]`) is *not* a cap truncation — the declared range\n * was delivered in full — so it falls through to `declared`.\n */\nfunction deliveredRange(text: string, declared: ReadRange): ReadRange {\n\t// The truncation notice is always the trailing `\\n\\n[Showing lines A-B of N ...]`\n\t// clause the read tool appends. Anchor to the end so a `[Showing lines ...]`\n\t// string that merely appears *inside* the file's content can't spoof it.\n\tconst showing = text.match(/\\n\\n\\[Showing lines (\\d+)-(\\d+) of \\d+[^\\]]*\\]\\s*$/);\n\tif (showing) {\n\t\tconst a = Number(showing[1]);\n\t\tconst b = Number(showing[2]);\n\t\tif (Number.isFinite(a) && Number.isFinite(b) && b >= a) return { start: a, end: b + 1 };\n\t}\n\t// First line alone exceeded the byte limit: the whole result *is* that notice,\n\t// so it must start the text. Nothing usable was delivered.\n\tif (/^\\[Line \\d+ is .+ exceeds .+ limit\\./.test(text)) return { start: 1, end: 1 };\n\treturn declared;\n}\n\n/**\n * Find the latest earlier read that (a) is for the same resolved path, (b)\n * actually delivered a range containing the requested range, and (c) is still\n * live in the outgoing context — i.e. the post-hoc GC will not have stubbed it,\n * because no later edit/write and no later overlapping content read supersede\n * it. Returns that read's delivered range for the pointer, or null when the\n * current read must actually run.\n *\n * Deliberately conservative: a truncated earlier read only covers what it\n * delivered, a whole-file read must have been delivered untruncated to count as\n * covering, and any pointer results (which fetched nothing) are ignored on both\n * the candidate and the supersession side.\n */\nexport function findCoveringRead(entries: readonly unknown[], opts: FindCoveringReadOptions): CoveringRead | null {\n\t// `declared` mirrors the range the GC uses for supersession (straight from the\n\t// call args); `delivered` is what the result text shows was actually returned,\n\t// used for coverage. The current call has no result yet, so it never appears.\n\tinterface PriorRead {\n\t\tindex: number;\n\t\tdeclared: ReadRange;\n\t\tdelivered: ReadRange;\n\t\tdisplay: string;\n\t\tcallId: string;\n\t}\n\n\t// Resolve each distinct raw path once — the resolver may hit the filesystem.\n\tconst resolveCache = new Map<string, string>();\n\tconst resolvePath = (raw: string): string => {\n\t\tconst hit = resolveCache.get(raw);\n\t\tif (hit !== undefined) return hit;\n\t\tconst resolved = opts.resolvePath(raw);\n\t\tresolveCache.set(raw, resolved);\n\t\treturn resolved;\n\t};\n\n\t// Single ordered pass. `readCall`/`mutateCallPath` map a call id to its path as\n\t// the call is seen (a toolCall always precedes its result); `reads` collects\n\t// the target path's reads and `lastMutateIndex` its last mutate. `order` gives\n\t// live-context position for the supersession/mutate comparisons.\n\tconst readCall = new Map<string, { resolved: string; display: string; declared: ReadRange }>();\n\tconst mutateCallPath = new Map<string, string>();\n\tconst reads: PriorRead[] = [];\n\tlet lastMutateIndex = -1;\n\t// A *failed* edit/write is not a mutate - the GC is right to ignore it, since\n\t// the file did not change. It is still the one moment the model most needs the\n\t// real bytes: the failure usually means its copy of the text does not match the\n\t// file. Serving a \"not re-fetched, unchanged\" pointer there leaves it retrying\n\t// the same wrong text with no way to see what is actually on disk.\n\tlet lastFailedMutateIndex = -1;\n\tlet order = 0;\n\n\tfor (const entry of entries) {\n\t\t// Compaction boundary: everything before it is replaced by a summary in the\n\t\t// live context, so drop the state accumulated so far. Conservative — the\n\t\t// kept tail before the boundary is dropped too — which can only miss a\n\t\t// dedup, never point at content that is no longer in context.\n\t\tif (isCompactionEntry(entry)) {\n\t\t\treadCall.clear();\n\t\t\tmutateCallPath.clear();\n\t\t\treads.length = 0;\n\t\t\tlastMutateIndex = -1;\n\t\t\torder = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tconst m = toMessage(entry);\n\t\tif (!m) continue;\n\t\tconst i = order++;\n\t\tif (m.role === \"assistant\" && Array.isArray(m.content)) {\n\t\t\tfor (const b of m.content) {\n\t\t\t\tif (b.type !== \"toolCall\" || !b.id) continue;\n\t\t\t\tconst raw = b.arguments?.path;\n\t\t\t\tif (typeof raw !== \"string\" || raw.length === 0) continue;\n\t\t\t\tconst resolved = resolvePath(raw);\n\t\t\t\tif (b.name === READ_TOOL) {\n\t\t\t\t\treadCall.set(b.id, { resolved, display: raw, declared: readRangeFromArgs(b.arguments) });\n\t\t\t\t} else if (b.name && MUTATE_TOOLS.has(b.name)) {\n\t\t\t\t\tmutateCallPath.set(b.id, resolved);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (m.role === \"toolResult\" && m.isError && m.toolCallId) {\n\t\t\tif (m.toolName && MUTATE_TOOLS.has(m.toolName) && mutateCallPath.get(m.toolCallId) === opts.resolvedPath) {\n\t\t\t\tlastFailedMutateIndex = i;\n\t\t\t}\n\t\t} else if (m.role === \"toolResult\" && !m.isError && m.toolCallId) {\n\t\t\tif (m.toolName === READ_TOOL) {\n\t\t\t\tif (m.toolCallId === opts.currentCallId) continue;\n\t\t\t\tconst info = readCall.get(m.toolCallId);\n\t\t\t\tif (!info || info.resolved !== opts.resolvedPath) continue;\n\t\t\t\tconst text = resultText(m);\n\t\t\t\t// A pointer fetched nothing: the GC excludes it from supersession, so we\n\t\t\t\t// must too (both as a candidate and as a superseder).\n\t\t\t\tif (isDedupPointerText(text)) continue;\n\t\t\t\treads.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\tdeclared: info.declared,\n\t\t\t\t\tdelivered: deliveredRange(text, info.declared),\n\t\t\t\t\tdisplay: info.display,\n\t\t\t\t\tcallId: m.toolCallId,\n\t\t\t\t});\n\t\t\t} else if (m.toolName && MUTATE_TOOLS.has(m.toolName)) {\n\t\t\t\tif (mutateCallPath.get(m.toolCallId) === opts.resolvedPath) lastMutateIndex = i;\n\t\t\t}\n\t\t}\n\t}\n\n\t// A read survives the GC iff no later mutate and no later read overlaps its\n\t// *declared* range — exactly the GC's own test — so predict it the same way.\n\tconst survivesGc = (r: PriorRead): boolean =>\n\t\tlastMutateIndex <= r.index && !reads.some((o) => o.index > r.index && rangesOverlap(o.declared, r.declared));\n\n\t// Nothing earlier can be trusted to satisfy this read once an edit against this\n\t// path has failed: let the read run and hand the model the current bytes.\n\tif (lastFailedMutateIndex > -1) return null;\n\n\tlet best: PriorRead | null = null;\n\tfor (const r of reads) {\n\t\t// Coverage uses the *delivered* range: a truncated read only holds what it returned.\n\t\tif (!rangeContains(r.delivered, opts.requestedRange)) continue;\n\t\tif (!survivesGc(r)) continue;\n\t\tif (!best || r.index > best.index) best = r;\n\t}\n\tif (!best) return null;\n\treturn { callId: best.callId, display: best.display, start: best.delivered.start, end: best.delivered.end };\n}\n\n/** The parts of a covering read the pointer message renders. */\nexport type CoveringReadDisplay = Pick<CoveringRead, \"display\" | \"start\" | \"end\">;\n\n/** Build the pointer text returned in place of a re-fetch. */\nexport function buildDedupPointerText(covering: CoveringReadDisplay): string {\n\tconst where =\n\t\tcovering.end === Number.POSITIVE_INFINITY\n\t\t\t? \"the entire file\"\n\t\t\t: covering.end - 1 > covering.start\n\t\t\t\t? `lines ${covering.start}-${covering.end - 1}`\n\t\t\t\t: `line ${covering.start}`;\n\treturn `${DEDUP_POINTER_PREFIX} ${covering.display} (${where}) was already read earlier in this session and has not changed since. Not re-fetched to save tokens — pass a different offset/limit, or edit the file, if you need other or newer content.]`;\n}\n"]}
|
|
@@ -49,6 +49,42 @@ export const DEDUP_POINTER_PREFIX = "[Already in context:";
|
|
|
49
49
|
export function isDedupPointerText(text) {
|
|
50
50
|
return text.trimStart().startsWith(DEDUP_POINTER_PREFIX);
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Content stamps for reads that have already been delivered.
|
|
54
|
+
*
|
|
55
|
+
* The dedup pointer tells the model the file "has not changed since" the read it
|
|
56
|
+
* names. Nothing in the transcript can establish that: an editor, a formatter, a
|
|
57
|
+
* branch switch or a second agent can rewrite the file between two reads without
|
|
58
|
+
* leaving a trace in it. So each delivered read records a stamp of the file it
|
|
59
|
+
* saw, and the pointer is only served when the file still stamps the same.
|
|
60
|
+
*
|
|
61
|
+
* Keyed by tool call id rather than by path, so two sessions sharing a process
|
|
62
|
+
* cannot overwrite each other's observations. Bounded, and a missing entry
|
|
63
|
+
* counts as "cannot prove it is unchanged" - the read then simply runs.
|
|
64
|
+
*/
|
|
65
|
+
const MAX_TRACKED_READS = 500;
|
|
66
|
+
const stampByCallId = new Map();
|
|
67
|
+
/** Remember the stamp of the file a read delivered. */
|
|
68
|
+
export function recordReadStamp(callId, stamp) {
|
|
69
|
+
if (stampByCallId.size >= MAX_TRACKED_READS) {
|
|
70
|
+
const oldest = stampByCallId.keys().next().value;
|
|
71
|
+
if (oldest !== undefined)
|
|
72
|
+
stampByCallId.delete(oldest);
|
|
73
|
+
}
|
|
74
|
+
stampByCallId.set(callId, stamp);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether the file a read delivered still stamps the same. False when no stamp
|
|
78
|
+
* was recorded, so an unprovable case re-reads rather than asserting freshness.
|
|
79
|
+
*/
|
|
80
|
+
export function readStampMatches(callId, stamp) {
|
|
81
|
+
const recorded = stampByCallId.get(callId);
|
|
82
|
+
return recorded !== undefined && recorded === stamp;
|
|
83
|
+
}
|
|
84
|
+
/** Test seam: drop all recorded stamps. */
|
|
85
|
+
export function clearReadStamps() {
|
|
86
|
+
stampByCallId.clear();
|
|
87
|
+
}
|
|
52
88
|
const READ_TOOL = "read";
|
|
53
89
|
const MUTATE_TOOLS = new Set(["edit", "write"]);
|
|
54
90
|
/** A session compaction entry — the boundary that trims the live context. */
|
|
@@ -131,6 +167,12 @@ export function findCoveringRead(entries, opts) {
|
|
|
131
167
|
const mutateCallPath = new Map();
|
|
132
168
|
const reads = [];
|
|
133
169
|
let lastMutateIndex = -1;
|
|
170
|
+
// A *failed* edit/write is not a mutate - the GC is right to ignore it, since
|
|
171
|
+
// the file did not change. It is still the one moment the model most needs the
|
|
172
|
+
// real bytes: the failure usually means its copy of the text does not match the
|
|
173
|
+
// file. Serving a "not re-fetched, unchanged" pointer there leaves it retrying
|
|
174
|
+
// the same wrong text with no way to see what is actually on disk.
|
|
175
|
+
let lastFailedMutateIndex = -1;
|
|
134
176
|
let order = 0;
|
|
135
177
|
for (const entry of entries) {
|
|
136
178
|
// Compaction boundary: everything before it is replaced by a summary in the
|
|
@@ -165,6 +207,11 @@ export function findCoveringRead(entries, opts) {
|
|
|
165
207
|
}
|
|
166
208
|
}
|
|
167
209
|
}
|
|
210
|
+
else if (m.role === "toolResult" && m.isError && m.toolCallId) {
|
|
211
|
+
if (m.toolName && MUTATE_TOOLS.has(m.toolName) && mutateCallPath.get(m.toolCallId) === opts.resolvedPath) {
|
|
212
|
+
lastFailedMutateIndex = i;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
168
215
|
else if (m.role === "toolResult" && !m.isError && m.toolCallId) {
|
|
169
216
|
if (m.toolName === READ_TOOL) {
|
|
170
217
|
if (m.toolCallId === opts.currentCallId)
|
|
@@ -182,6 +229,7 @@ export function findCoveringRead(entries, opts) {
|
|
|
182
229
|
declared: info.declared,
|
|
183
230
|
delivered: deliveredRange(text, info.declared),
|
|
184
231
|
display: info.display,
|
|
232
|
+
callId: m.toolCallId,
|
|
185
233
|
});
|
|
186
234
|
}
|
|
187
235
|
else if (m.toolName && MUTATE_TOOLS.has(m.toolName)) {
|
|
@@ -193,6 +241,10 @@ export function findCoveringRead(entries, opts) {
|
|
|
193
241
|
// A read survives the GC iff no later mutate and no later read overlaps its
|
|
194
242
|
// *declared* range — exactly the GC's own test — so predict it the same way.
|
|
195
243
|
const survivesGc = (r) => lastMutateIndex <= r.index && !reads.some((o) => o.index > r.index && rangesOverlap(o.declared, r.declared));
|
|
244
|
+
// Nothing earlier can be trusted to satisfy this read once an edit against this
|
|
245
|
+
// path has failed: let the read run and hand the model the current bytes.
|
|
246
|
+
if (lastFailedMutateIndex > -1)
|
|
247
|
+
return null;
|
|
196
248
|
let best = null;
|
|
197
249
|
for (const r of reads) {
|
|
198
250
|
// Coverage uses the *delivered* range: a truncated read only holds what it returned.
|
|
@@ -205,7 +257,7 @@ export function findCoveringRead(entries, opts) {
|
|
|
205
257
|
}
|
|
206
258
|
if (!best)
|
|
207
259
|
return null;
|
|
208
|
-
return { display: best.display, start: best.delivered.start, end: best.delivered.end };
|
|
260
|
+
return { callId: best.callId, display: best.display, start: best.delivered.start, end: best.delivered.end };
|
|
209
261
|
}
|
|
210
262
|
/** Build the pointer text returned in place of a re-fetch. */
|
|
211
263
|
export function buildDedupPointerText(covering) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-dedup.js","sourceRoot":"","sources":["../../../src/core/tools/read-dedup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read de-duplication primitives.\n *\n * Two mechanisms share this module:\n *\n * - The post-hoc context GC (`context-gc.ts`), which stubs out a `read` result\n * once a later overlapping read or an edit/write has superseded it.\n * - The at-call-time guard in the `read` tool, which short-circuits a read whose\n * requested range is *already fully covered* by an earlier, still-live read in\n * the current session, returning a pointer instead of re-fetching the file.\n *\n * Both reason about half-open line ranges `[start, end)` (end exclusive), so the\n * range math lives here and cannot drift between them. The guard additionally\n * needs to (a) recognise its own pointer results so they are never treated as\n * content-bearing reads, and (b) walk the session branch to find a covering\n * read — both of which are defined here to keep the tool file lean.\n */\n\n/** Half-open line interval `[start, end)` a read call covers; end is exclusive. */\nexport interface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/** A read with no offset/limit covers the whole file (open-ended). */\nexport const WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nexport function readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nexport function rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\n/** Whether `outer` fully contains `inner` (every line of inner lies within outer). */\nfunction rangeContains(outer: ReadRange, inner: ReadRange): boolean {\n\treturn outer.start <= inner.start && outer.end >= inner.end;\n}\n\n/**\n * Marker prefix for the at-call dedup pointer. Kept stable so the context GC can\n * recognise a pointer result and exclude it from supersession bookkeeping (a\n * pointer fetched no content, so it must not stub the read it points at).\n */\nexport const DEDUP_POINTER_PREFIX = \"[Already in context:\";\n\n/** Whether a tool-result text is an at-call dedup pointer. */\nexport function isDedupPointerText(text: string): boolean {\n\treturn text.trimStart().startsWith(DEDUP_POINTER_PREFIX);\n}\n\n/** A covering earlier read, described for the pointer message. */\nexport interface CoveringRead {\n\t/** The path as the earlier read spelled it (for a friendly pointer). */\n\tdisplay: string;\n\t/** Delivered range start (1-indexed line). */\n\tstart: number;\n\t/** Delivered range end (exclusive; Infinity for a whole-file read). */\n\tend: number;\n}\n\nexport interface FindCoveringReadOptions {\n\t/** Resolved absolute path of the current read. */\n\tresolvedPath: string;\n\t/** Range the current read is asking for. */\n\trequestedRange: ReadRange;\n\t/** Tool call id of the current read, excluded from candidate/supersession sets. */\n\tcurrentCallId: string;\n\t/** Resolve a raw read-arg path the same way the current read resolved its path. */\n\tresolvePath: (rawPath: string) => string;\n}\n\ninterface ContentBlock {\n\ttype?: string;\n\tid?: string;\n\tname?: string;\n\targuments?: Record<string, unknown>;\n\ttext?: string;\n}\n\ninterface MessageLike {\n\trole?: string;\n\tcontent?: ContentBlock[];\n\ttoolCallId?: string;\n\ttoolName?: string;\n\tisError?: boolean;\n}\n\nconst READ_TOOL = \"read\";\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** A session compaction entry — the boundary that trims the live context. */\nfunction isCompactionEntry(entry: unknown): boolean {\n\treturn !!entry && typeof entry === \"object\" && (entry as { type?: unknown }).type === \"compaction\";\n}\n\n/** Accept either raw `AgentMessage`s or session entries wrapping `.message`. */\nfunction toMessage(entry: unknown): MessageLike | null {\n\tif (!entry || typeof entry !== \"object\") return null;\n\tconst e = entry as { message?: unknown; role?: unknown };\n\tif (e.message && typeof e.message === \"object\") return e.message as MessageLike;\n\tif (typeof e.role === \"string\") return e as MessageLike;\n\treturn null;\n}\n\nfunction resultText(m: MessageLike): string {\n\treturn (m.content ?? [])\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text ?? \"\")\n\t\t.join(\"\");\n}\n\n/**\n * Recover the range a read result actually *delivered* from its text.\n *\n * A cap-truncated read announces `[Showing lines A-B of N ...]`, so it delivered\n * only `[A, B+1)` even though its args declared a wider range. A read whose first\n * line alone exceeded the byte cap delivered nothing. Any other (untruncated)\n * read delivered its full declared range. The user-`limit` early-stop notice\n * (`[N more lines in file ...]`) is *not* a cap truncation — the declared range\n * was delivered in full — so it falls through to `declared`.\n */\nfunction deliveredRange(text: string, declared: ReadRange): ReadRange {\n\t// The truncation notice is always the trailing `\\n\\n[Showing lines A-B of N ...]`\n\t// clause the read tool appends. Anchor to the end so a `[Showing lines ...]`\n\t// string that merely appears *inside* the file's content can't spoof it.\n\tconst showing = text.match(/\\n\\n\\[Showing lines (\\d+)-(\\d+) of \\d+[^\\]]*\\]\\s*$/);\n\tif (showing) {\n\t\tconst a = Number(showing[1]);\n\t\tconst b = Number(showing[2]);\n\t\tif (Number.isFinite(a) && Number.isFinite(b) && b >= a) return { start: a, end: b + 1 };\n\t}\n\t// First line alone exceeded the byte limit: the whole result *is* that notice,\n\t// so it must start the text. Nothing usable was delivered.\n\tif (/^\\[Line \\d+ is .+ exceeds .+ limit\\./.test(text)) return { start: 1, end: 1 };\n\treturn declared;\n}\n\n/**\n * Find the latest earlier read that (a) is for the same resolved path, (b)\n * actually delivered a range containing the requested range, and (c) is still\n * live in the outgoing context — i.e. the post-hoc GC will not have stubbed it,\n * because no later edit/write and no later overlapping content read supersede\n * it. Returns that read's delivered range for the pointer, or null when the\n * current read must actually run.\n *\n * Deliberately conservative: a truncated earlier read only covers what it\n * delivered, a whole-file read must have been delivered untruncated to count as\n * covering, and any pointer results (which fetched nothing) are ignored on both\n * the candidate and the supersession side.\n */\nexport function findCoveringRead(entries: readonly unknown[], opts: FindCoveringReadOptions): CoveringRead | null {\n\t// `declared` mirrors the range the GC uses for supersession (straight from the\n\t// call args); `delivered` is what the result text shows was actually returned,\n\t// used for coverage. The current call has no result yet, so it never appears.\n\tinterface PriorRead {\n\t\tindex: number;\n\t\tdeclared: ReadRange;\n\t\tdelivered: ReadRange;\n\t\tdisplay: string;\n\t}\n\n\t// Resolve each distinct raw path once — the resolver may hit the filesystem.\n\tconst resolveCache = new Map<string, string>();\n\tconst resolvePath = (raw: string): string => {\n\t\tconst hit = resolveCache.get(raw);\n\t\tif (hit !== undefined) return hit;\n\t\tconst resolved = opts.resolvePath(raw);\n\t\tresolveCache.set(raw, resolved);\n\t\treturn resolved;\n\t};\n\n\t// Single ordered pass. `readCall`/`mutateCallPath` map a call id to its path as\n\t// the call is seen (a toolCall always precedes its result); `reads` collects\n\t// the target path's reads and `lastMutateIndex` its last mutate. `order` gives\n\t// live-context position for the supersession/mutate comparisons.\n\tconst readCall = new Map<string, { resolved: string; display: string; declared: ReadRange }>();\n\tconst mutateCallPath = new Map<string, string>();\n\tconst reads: PriorRead[] = [];\n\tlet lastMutateIndex = -1;\n\tlet order = 0;\n\n\tfor (const entry of entries) {\n\t\t// Compaction boundary: everything before it is replaced by a summary in the\n\t\t// live context, so drop the state accumulated so far. Conservative — the\n\t\t// kept tail before the boundary is dropped too — which can only miss a\n\t\t// dedup, never point at content that is no longer in context.\n\t\tif (isCompactionEntry(entry)) {\n\t\t\treadCall.clear();\n\t\t\tmutateCallPath.clear();\n\t\t\treads.length = 0;\n\t\t\tlastMutateIndex = -1;\n\t\t\torder = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tconst m = toMessage(entry);\n\t\tif (!m) continue;\n\t\tconst i = order++;\n\t\tif (m.role === \"assistant\" && Array.isArray(m.content)) {\n\t\t\tfor (const b of m.content) {\n\t\t\t\tif (b.type !== \"toolCall\" || !b.id) continue;\n\t\t\t\tconst raw = b.arguments?.path;\n\t\t\t\tif (typeof raw !== \"string\" || raw.length === 0) continue;\n\t\t\t\tconst resolved = resolvePath(raw);\n\t\t\t\tif (b.name === READ_TOOL) {\n\t\t\t\t\treadCall.set(b.id, { resolved, display: raw, declared: readRangeFromArgs(b.arguments) });\n\t\t\t\t} else if (b.name && MUTATE_TOOLS.has(b.name)) {\n\t\t\t\t\tmutateCallPath.set(b.id, resolved);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (m.role === \"toolResult\" && !m.isError && m.toolCallId) {\n\t\t\tif (m.toolName === READ_TOOL) {\n\t\t\t\tif (m.toolCallId === opts.currentCallId) continue;\n\t\t\t\tconst info = readCall.get(m.toolCallId);\n\t\t\t\tif (!info || info.resolved !== opts.resolvedPath) continue;\n\t\t\t\tconst text = resultText(m);\n\t\t\t\t// A pointer fetched nothing: the GC excludes it from supersession, so we\n\t\t\t\t// must too (both as a candidate and as a superseder).\n\t\t\t\tif (isDedupPointerText(text)) continue;\n\t\t\t\treads.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\tdeclared: info.declared,\n\t\t\t\t\tdelivered: deliveredRange(text, info.declared),\n\t\t\t\t\tdisplay: info.display,\n\t\t\t\t});\n\t\t\t} else if (m.toolName && MUTATE_TOOLS.has(m.toolName)) {\n\t\t\t\tif (mutateCallPath.get(m.toolCallId) === opts.resolvedPath) lastMutateIndex = i;\n\t\t\t}\n\t\t}\n\t}\n\n\t// A read survives the GC iff no later mutate and no later read overlaps its\n\t// *declared* range — exactly the GC's own test — so predict it the same way.\n\tconst survivesGc = (r: PriorRead): boolean =>\n\t\tlastMutateIndex <= r.index && !reads.some((o) => o.index > r.index && rangesOverlap(o.declared, r.declared));\n\n\tlet best: PriorRead | null = null;\n\tfor (const r of reads) {\n\t\t// Coverage uses the *delivered* range: a truncated read only holds what it returned.\n\t\tif (!rangeContains(r.delivered, opts.requestedRange)) continue;\n\t\tif (!survivesGc(r)) continue;\n\t\tif (!best || r.index > best.index) best = r;\n\t}\n\tif (!best) return null;\n\treturn { display: best.display, start: best.delivered.start, end: best.delivered.end };\n}\n\n/** Build the pointer text returned in place of a re-fetch. */\nexport function buildDedupPointerText(covering: CoveringRead): string {\n\tconst where =\n\t\tcovering.end === Number.POSITIVE_INFINITY\n\t\t\t? \"the entire file\"\n\t\t\t: covering.end - 1 > covering.start\n\t\t\t\t? `lines ${covering.start}-${covering.end - 1}`\n\t\t\t\t: `line ${covering.start}`;\n\treturn `${DEDUP_POINTER_PREFIX} ${covering.display} (${where}) was already read earlier in this session and has not changed since. Not re-fetched to save tokens — pass a different offset/limit, or edit the file, if you need other or newer content.]`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read-dedup.js","sourceRoot":"","sources":["../../../src/core/tools/read-dedup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read de-duplication primitives.\n *\n * Two mechanisms share this module:\n *\n * - The post-hoc context GC (`context-gc.ts`), which stubs out a `read` result\n * once a later overlapping read or an edit/write has superseded it.\n * - The at-call-time guard in the `read` tool, which short-circuits a read whose\n * requested range is *already fully covered* by an earlier, still-live read in\n * the current session, returning a pointer instead of re-fetching the file.\n *\n * Both reason about half-open line ranges `[start, end)` (end exclusive), so the\n * range math lives here and cannot drift between them. The guard additionally\n * needs to (a) recognise its own pointer results so they are never treated as\n * content-bearing reads, and (b) walk the session branch to find a covering\n * read — both of which are defined here to keep the tool file lean.\n */\n\n/** Half-open line interval `[start, end)` a read call covers; end is exclusive. */\nexport interface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/** A read with no offset/limit covers the whole file (open-ended). */\nexport const WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nexport function readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nexport function rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\n/** Whether `outer` fully contains `inner` (every line of inner lies within outer). */\nfunction rangeContains(outer: ReadRange, inner: ReadRange): boolean {\n\treturn outer.start <= inner.start && outer.end >= inner.end;\n}\n\n/**\n * Marker prefix for the at-call dedup pointer. Kept stable so the context GC can\n * recognise a pointer result and exclude it from supersession bookkeeping (a\n * pointer fetched no content, so it must not stub the read it points at).\n */\nexport const DEDUP_POINTER_PREFIX = \"[Already in context:\";\n\n/** Whether a tool-result text is an at-call dedup pointer. */\nexport function isDedupPointerText(text: string): boolean {\n\treturn text.trimStart().startsWith(DEDUP_POINTER_PREFIX);\n}\n\n/**\n * Content stamps for reads that have already been delivered.\n *\n * The dedup pointer tells the model the file \"has not changed since\" the read it\n * names. Nothing in the transcript can establish that: an editor, a formatter, a\n * branch switch or a second agent can rewrite the file between two reads without\n * leaving a trace in it. So each delivered read records a stamp of the file it\n * saw, and the pointer is only served when the file still stamps the same.\n *\n * Keyed by tool call id rather than by path, so two sessions sharing a process\n * cannot overwrite each other's observations. Bounded, and a missing entry\n * counts as \"cannot prove it is unchanged\" - the read then simply runs.\n */\nconst MAX_TRACKED_READS = 500;\nconst stampByCallId = new Map<string, string>();\n\n/** Remember the stamp of the file a read delivered. */\nexport function recordReadStamp(callId: string, stamp: string): void {\n\tif (stampByCallId.size >= MAX_TRACKED_READS) {\n\t\tconst oldest = stampByCallId.keys().next().value;\n\t\tif (oldest !== undefined) stampByCallId.delete(oldest);\n\t}\n\tstampByCallId.set(callId, stamp);\n}\n\n/**\n * Whether the file a read delivered still stamps the same. False when no stamp\n * was recorded, so an unprovable case re-reads rather than asserting freshness.\n */\nexport function readStampMatches(callId: string, stamp: string): boolean {\n\tconst recorded = stampByCallId.get(callId);\n\treturn recorded !== undefined && recorded === stamp;\n}\n\n/** Test seam: drop all recorded stamps. */\nexport function clearReadStamps(): void {\n\tstampByCallId.clear();\n}\n\n/** A covering earlier read, described for the pointer message. */\nexport interface CoveringRead {\n\t/** Tool call id of the read being pointed at, used to check its stamp. */\n\tcallId: string;\n\t/** The path as the earlier read spelled it (for a friendly pointer). */\n\tdisplay: string;\n\t/** Delivered range start (1-indexed line). */\n\tstart: number;\n\t/** Delivered range end (exclusive; Infinity for a whole-file read). */\n\tend: number;\n}\n\nexport interface FindCoveringReadOptions {\n\t/** Resolved absolute path of the current read. */\n\tresolvedPath: string;\n\t/** Range the current read is asking for. */\n\trequestedRange: ReadRange;\n\t/** Tool call id of the current read, excluded from candidate/supersession sets. */\n\tcurrentCallId: string;\n\t/** Resolve a raw read-arg path the same way the current read resolved its path. */\n\tresolvePath: (rawPath: string) => string;\n}\n\ninterface ContentBlock {\n\ttype?: string;\n\tid?: string;\n\tname?: string;\n\targuments?: Record<string, unknown>;\n\ttext?: string;\n}\n\ninterface MessageLike {\n\trole?: string;\n\tcontent?: ContentBlock[];\n\ttoolCallId?: string;\n\ttoolName?: string;\n\tisError?: boolean;\n}\n\nconst READ_TOOL = \"read\";\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** A session compaction entry — the boundary that trims the live context. */\nfunction isCompactionEntry(entry: unknown): boolean {\n\treturn !!entry && typeof entry === \"object\" && (entry as { type?: unknown }).type === \"compaction\";\n}\n\n/** Accept either raw `AgentMessage`s or session entries wrapping `.message`. */\nfunction toMessage(entry: unknown): MessageLike | null {\n\tif (!entry || typeof entry !== \"object\") return null;\n\tconst e = entry as { message?: unknown; role?: unknown };\n\tif (e.message && typeof e.message === \"object\") return e.message as MessageLike;\n\tif (typeof e.role === \"string\") return e as MessageLike;\n\treturn null;\n}\n\nfunction resultText(m: MessageLike): string {\n\treturn (m.content ?? [])\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text ?? \"\")\n\t\t.join(\"\");\n}\n\n/**\n * Recover the range a read result actually *delivered* from its text.\n *\n * A cap-truncated read announces `[Showing lines A-B of N ...]`, so it delivered\n * only `[A, B+1)` even though its args declared a wider range. A read whose first\n * line alone exceeded the byte cap delivered nothing. Any other (untruncated)\n * read delivered its full declared range. The user-`limit` early-stop notice\n * (`[N more lines in file ...]`) is *not* a cap truncation — the declared range\n * was delivered in full — so it falls through to `declared`.\n */\nfunction deliveredRange(text: string, declared: ReadRange): ReadRange {\n\t// The truncation notice is always the trailing `\\n\\n[Showing lines A-B of N ...]`\n\t// clause the read tool appends. Anchor to the end so a `[Showing lines ...]`\n\t// string that merely appears *inside* the file's content can't spoof it.\n\tconst showing = text.match(/\\n\\n\\[Showing lines (\\d+)-(\\d+) of \\d+[^\\]]*\\]\\s*$/);\n\tif (showing) {\n\t\tconst a = Number(showing[1]);\n\t\tconst b = Number(showing[2]);\n\t\tif (Number.isFinite(a) && Number.isFinite(b) && b >= a) return { start: a, end: b + 1 };\n\t}\n\t// First line alone exceeded the byte limit: the whole result *is* that notice,\n\t// so it must start the text. Nothing usable was delivered.\n\tif (/^\\[Line \\d+ is .+ exceeds .+ limit\\./.test(text)) return { start: 1, end: 1 };\n\treturn declared;\n}\n\n/**\n * Find the latest earlier read that (a) is for the same resolved path, (b)\n * actually delivered a range containing the requested range, and (c) is still\n * live in the outgoing context — i.e. the post-hoc GC will not have stubbed it,\n * because no later edit/write and no later overlapping content read supersede\n * it. Returns that read's delivered range for the pointer, or null when the\n * current read must actually run.\n *\n * Deliberately conservative: a truncated earlier read only covers what it\n * delivered, a whole-file read must have been delivered untruncated to count as\n * covering, and any pointer results (which fetched nothing) are ignored on both\n * the candidate and the supersession side.\n */\nexport function findCoveringRead(entries: readonly unknown[], opts: FindCoveringReadOptions): CoveringRead | null {\n\t// `declared` mirrors the range the GC uses for supersession (straight from the\n\t// call args); `delivered` is what the result text shows was actually returned,\n\t// used for coverage. The current call has no result yet, so it never appears.\n\tinterface PriorRead {\n\t\tindex: number;\n\t\tdeclared: ReadRange;\n\t\tdelivered: ReadRange;\n\t\tdisplay: string;\n\t\tcallId: string;\n\t}\n\n\t// Resolve each distinct raw path once — the resolver may hit the filesystem.\n\tconst resolveCache = new Map<string, string>();\n\tconst resolvePath = (raw: string): string => {\n\t\tconst hit = resolveCache.get(raw);\n\t\tif (hit !== undefined) return hit;\n\t\tconst resolved = opts.resolvePath(raw);\n\t\tresolveCache.set(raw, resolved);\n\t\treturn resolved;\n\t};\n\n\t// Single ordered pass. `readCall`/`mutateCallPath` map a call id to its path as\n\t// the call is seen (a toolCall always precedes its result); `reads` collects\n\t// the target path's reads and `lastMutateIndex` its last mutate. `order` gives\n\t// live-context position for the supersession/mutate comparisons.\n\tconst readCall = new Map<string, { resolved: string; display: string; declared: ReadRange }>();\n\tconst mutateCallPath = new Map<string, string>();\n\tconst reads: PriorRead[] = [];\n\tlet lastMutateIndex = -1;\n\t// A *failed* edit/write is not a mutate - the GC is right to ignore it, since\n\t// the file did not change. It is still the one moment the model most needs the\n\t// real bytes: the failure usually means its copy of the text does not match the\n\t// file. Serving a \"not re-fetched, unchanged\" pointer there leaves it retrying\n\t// the same wrong text with no way to see what is actually on disk.\n\tlet lastFailedMutateIndex = -1;\n\tlet order = 0;\n\n\tfor (const entry of entries) {\n\t\t// Compaction boundary: everything before it is replaced by a summary in the\n\t\t// live context, so drop the state accumulated so far. Conservative — the\n\t\t// kept tail before the boundary is dropped too — which can only miss a\n\t\t// dedup, never point at content that is no longer in context.\n\t\tif (isCompactionEntry(entry)) {\n\t\t\treadCall.clear();\n\t\t\tmutateCallPath.clear();\n\t\t\treads.length = 0;\n\t\t\tlastMutateIndex = -1;\n\t\t\torder = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tconst m = toMessage(entry);\n\t\tif (!m) continue;\n\t\tconst i = order++;\n\t\tif (m.role === \"assistant\" && Array.isArray(m.content)) {\n\t\t\tfor (const b of m.content) {\n\t\t\t\tif (b.type !== \"toolCall\" || !b.id) continue;\n\t\t\t\tconst raw = b.arguments?.path;\n\t\t\t\tif (typeof raw !== \"string\" || raw.length === 0) continue;\n\t\t\t\tconst resolved = resolvePath(raw);\n\t\t\t\tif (b.name === READ_TOOL) {\n\t\t\t\t\treadCall.set(b.id, { resolved, display: raw, declared: readRangeFromArgs(b.arguments) });\n\t\t\t\t} else if (b.name && MUTATE_TOOLS.has(b.name)) {\n\t\t\t\t\tmutateCallPath.set(b.id, resolved);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (m.role === \"toolResult\" && m.isError && m.toolCallId) {\n\t\t\tif (m.toolName && MUTATE_TOOLS.has(m.toolName) && mutateCallPath.get(m.toolCallId) === opts.resolvedPath) {\n\t\t\t\tlastFailedMutateIndex = i;\n\t\t\t}\n\t\t} else if (m.role === \"toolResult\" && !m.isError && m.toolCallId) {\n\t\t\tif (m.toolName === READ_TOOL) {\n\t\t\t\tif (m.toolCallId === opts.currentCallId) continue;\n\t\t\t\tconst info = readCall.get(m.toolCallId);\n\t\t\t\tif (!info || info.resolved !== opts.resolvedPath) continue;\n\t\t\t\tconst text = resultText(m);\n\t\t\t\t// A pointer fetched nothing: the GC excludes it from supersession, so we\n\t\t\t\t// must too (both as a candidate and as a superseder).\n\t\t\t\tif (isDedupPointerText(text)) continue;\n\t\t\t\treads.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\tdeclared: info.declared,\n\t\t\t\t\tdelivered: deliveredRange(text, info.declared),\n\t\t\t\t\tdisplay: info.display,\n\t\t\t\t\tcallId: m.toolCallId,\n\t\t\t\t});\n\t\t\t} else if (m.toolName && MUTATE_TOOLS.has(m.toolName)) {\n\t\t\t\tif (mutateCallPath.get(m.toolCallId) === opts.resolvedPath) lastMutateIndex = i;\n\t\t\t}\n\t\t}\n\t}\n\n\t// A read survives the GC iff no later mutate and no later read overlaps its\n\t// *declared* range — exactly the GC's own test — so predict it the same way.\n\tconst survivesGc = (r: PriorRead): boolean =>\n\t\tlastMutateIndex <= r.index && !reads.some((o) => o.index > r.index && rangesOverlap(o.declared, r.declared));\n\n\t// Nothing earlier can be trusted to satisfy this read once an edit against this\n\t// path has failed: let the read run and hand the model the current bytes.\n\tif (lastFailedMutateIndex > -1) return null;\n\n\tlet best: PriorRead | null = null;\n\tfor (const r of reads) {\n\t\t// Coverage uses the *delivered* range: a truncated read only holds what it returned.\n\t\tif (!rangeContains(r.delivered, opts.requestedRange)) continue;\n\t\tif (!survivesGc(r)) continue;\n\t\tif (!best || r.index > best.index) best = r;\n\t}\n\tif (!best) return null;\n\treturn { callId: best.callId, display: best.display, start: best.delivered.start, end: best.delivered.end };\n}\n\n/** The parts of a covering read the pointer message renders. */\nexport type CoveringReadDisplay = Pick<CoveringRead, \"display\" | \"start\" | \"end\">;\n\n/** Build the pointer text returned in place of a re-fetch. */\nexport function buildDedupPointerText(covering: CoveringReadDisplay): string {\n\tconst where =\n\t\tcovering.end === Number.POSITIVE_INFINITY\n\t\t\t? \"the entire file\"\n\t\t\t: covering.end - 1 > covering.start\n\t\t\t\t? `lines ${covering.start}-${covering.end - 1}`\n\t\t\t\t: `line ${covering.start}`;\n\treturn `${DEDUP_POINTER_PREFIX} ${covering.display} (${where}) was already read earlier in this session and has not changed since. Not re-fetched to save tokens — pass a different offset/limit, or edit the file, if you need other or newer content.]`;\n}\n"]}
|