|
1 | | -import { stat } from "fs/promises" |
| 1 | +import fs from "fs" |
2 | 2 |
|
| 3 | +// Types |
3 | 4 | export interface LineRange { |
4 | 5 | start: number |
5 | 6 | end: number |
6 | 7 | } |
7 | 8 |
|
8 | 9 | export interface FileMetadata { |
9 | 10 | fileName: string |
10 | | - mtime: number |
11 | | - loadedRanges: LineRange[] |
| 11 | + mtime: string |
| 12 | + lineRanges: LineRange[] |
12 | 13 | } |
13 | 14 |
|
14 | 15 | export interface ConversationMessage { |
| 16 | + role: "user" | "assistant" |
| 17 | + content: any |
| 18 | + ts: number |
| 19 | + tool?: { |
| 20 | + name: string |
| 21 | + options: any |
| 22 | + } |
15 | 23 | files?: FileMetadata[] |
16 | 24 | } |
17 | 25 |
|
18 | | -export interface FilteredReadRequest { |
19 | | - status: "REJECT_ALL" | "ALLOW_PARTIAL" | "ALLOW_ALL" |
20 | | - rangesToRead: LineRange[] |
21 | | -} |
| 26 | +type CacheResult = |
| 27 | + | { status: "ALLOW_ALL" } |
| 28 | + | { status: "ALLOW_PARTIAL"; rangesToRead: LineRange[] } |
| 29 | + | { status: "REJECT_ALL" } |
22 | 30 |
|
23 | 31 | /** |
24 | | - * Checks if two ranges overlap. |
| 32 | + * Checks if two line ranges overlap. |
| 33 | + * @param r1 - The first line range. |
| 34 | + * @param r2 - The second line range. |
| 35 | + * @returns True if the ranges overlap, false otherwise. |
25 | 36 | */ |
26 | 37 | function rangesOverlap(r1: LineRange, r2: LineRange): boolean { |
27 | 38 | return r1.start <= r2.end && r1.end >= r2.start |
28 | 39 | } |
29 | 40 |
|
30 | 41 | /** |
31 | | - * Subtracts one range from another. |
32 | | - * Returns an array of ranges that are in `original` but not in `toRemove`. |
| 42 | + * Subtracts one line range from another. |
| 43 | + * @param from - The range to subtract from. |
| 44 | + * @param toSubtract - The range to subtract. |
| 45 | + * @returns An array of ranges remaining after subtraction. |
33 | 46 | */ |
34 | | -export function subtractRange(original: LineRange, toRemove: LineRange): LineRange[] { |
35 | | - if (!rangesOverlap(original, toRemove)) { |
36 | | - return [original] |
| 47 | +function subtractRange(from: LineRange, toSubtract: LineRange): LineRange[] { |
| 48 | + // No overlap |
| 49 | + if (from.end < toSubtract.start || from.start > toSubtract.end) { |
| 50 | + return [from] |
37 | 51 | } |
38 | | - |
39 | | - const result: LineRange[] = [] |
40 | | - |
41 | | - // Part of original before toRemove |
42 | | - if (original.start < toRemove.start) { |
43 | | - result.push({ start: original.start, end: toRemove.start - 1 }) |
| 52 | + const remainingRanges: LineRange[] = [] |
| 53 | + // Part of 'from' is before 'toSubtract' |
| 54 | + if (from.start < toSubtract.start) { |
| 55 | + remainingRanges.push({ start: from.start, end: toSubtract.start - 1 }) |
44 | 56 | } |
45 | | - |
46 | | - // Part of original after toRemove |
47 | | - if (original.end > toRemove.end) { |
48 | | - result.push({ start: toRemove.end + 1, end: original.end }) |
| 57 | + // Part of 'from' is after 'toSubtract' |
| 58 | + if (from.end > toSubtract.end) { |
| 59 | + remainingRanges.push({ start: toSubtract.end + 1, end: from.end }) |
49 | 60 | } |
50 | | - |
51 | | - return result |
| 61 | + return remainingRanges |
52 | 62 | } |
53 | 63 |
|
54 | 64 | /** |
55 | | - * Subtracts a set of ranges from another set of ranges. |
| 65 | + * Processes a read request against cached file data in conversation history. |
| 66 | + * @param requestedFilePath - The full path of the file being requested. |
| 67 | + * @param requestedRanges - The line ranges being requested. |
| 68 | + * @param conversationHistory - The history of conversation messages. |
| 69 | + * @returns A CacheResult indicating whether to allow, partially allow, or reject the read. |
56 | 70 | */ |
57 | | -export function subtractRanges(originals: LineRange[], toRemoves: LineRange[]): LineRange[] { |
58 | | - let remaining = [...originals] |
59 | | - |
60 | | - for (const toRemove of toRemoves) { |
61 | | - remaining = remaining.flatMap((original) => subtractRange(original, toRemove)) |
62 | | - } |
63 | | - |
64 | | - return remaining |
65 | | -} |
66 | | - |
67 | 71 | export async function processAndFilterReadRequest( |
68 | | - requestedFile: string, |
| 72 | + requestedFilePath: string, |
69 | 73 | requestedRanges: LineRange[], |
70 | 74 | conversationHistory: ConversationMessage[], |
71 | | -): Promise<FilteredReadRequest> { |
72 | | - let currentMtime: number |
| 75 | +): Promise<CacheResult> { |
73 | 76 | try { |
74 | | - currentMtime = (await stat(requestedFile)).mtime.getTime() |
75 | | - } catch (error) { |
76 | | - // File doesn't exist or other error, so we must read. |
77 | | - return { |
78 | | - status: "ALLOW_ALL", |
79 | | - rangesToRead: requestedRanges, |
| 77 | + const stats = await fs.promises.stat(requestedFilePath) |
| 78 | + const currentMtime = stats.mtime.toISOString() |
| 79 | + |
| 80 | + let rangesToRead = [...requestedRanges] |
| 81 | + |
| 82 | + // If no specific ranges are requested, treat it as a request for the whole file. |
| 83 | + if (rangesToRead.length === 0) { |
| 84 | + // We need to know the number of lines to create a full range. |
| 85 | + // This logic is simplified; in a real scenario, you'd get the line count. |
| 86 | + // For this example, we'll assume we can't determine the full range without reading the file, |
| 87 | + // so we proceed with ALLOW_ALL if no ranges are specified. |
| 88 | + return { status: "ALLOW_ALL" } |
80 | 89 | } |
81 | | - } |
82 | | - |
83 | | - let rangesThatStillNeedToBeRead = [...requestedRanges] |
84 | | - |
85 | | - for (let i = conversationHistory.length - 1; i >= 0; i--) { |
86 | | - const message = conversationHistory[i] |
87 | | - if (!message.files) { |
88 | | - continue |
89 | | - } |
90 | | - |
91 | | - const relevantFileHistory = message.files.find((f) => f.fileName === requestedFile) |
92 | | - |
93 | | - if (relevantFileHistory && relevantFileHistory.mtime >= currentMtime) { |
94 | | - rangesThatStillNeedToBeRead = subtractRanges(rangesThatStillNeedToBeRead, relevantFileHistory.loadedRanges) |
95 | 90 |
|
96 | | - if (rangesThatStillNeedToBeRead.length === 0) { |
97 | | - return { |
98 | | - status: "REJECT_ALL", |
99 | | - rangesToRead: [], |
| 91 | + for (const message of conversationHistory) { |
| 92 | + if (message.files) { |
| 93 | + for (const file of message.files) { |
| 94 | + if (file.fileName === requestedFilePath && new Date(file.mtime) >= new Date(currentMtime)) { |
| 95 | + // File in history is up-to-date. Check ranges. |
| 96 | + for (const cachedRange of file.lineRanges) { |
| 97 | + rangesToRead = rangesToRead.flatMap((reqRange) => { |
| 98 | + if (rangesOverlap(reqRange, cachedRange)) { |
| 99 | + return subtractRange(reqRange, cachedRange) |
| 100 | + } |
| 101 | + return [reqRange] |
| 102 | + }) |
| 103 | + } |
| 104 | + } |
100 | 105 | } |
101 | 106 | } |
102 | 107 | } |
103 | | - } |
104 | | - |
105 | | - const originalRangesString = JSON.stringify(requestedRanges.sort((a, b) => a.start - b.start)) |
106 | | - const finalRangesString = JSON.stringify(rangesThatStillNeedToBeRead.sort((a, b) => a.start - b.start)) |
107 | 108 |
|
108 | | - if (originalRangesString === finalRangesString) { |
109 | | - return { |
110 | | - status: "ALLOW_ALL", |
111 | | - rangesToRead: requestedRanges, |
| 109 | + if (rangesToRead.length === 0) { |
| 110 | + return { status: "REJECT_ALL" } |
| 111 | + } else if (rangesToRead.length < requestedRanges.length) { |
| 112 | + return { status: "ALLOW_PARTIAL", rangesToRead } |
| 113 | + } else { |
| 114 | + return { status: "ALLOW_ALL" } |
112 | 115 | } |
113 | | - } |
114 | | - |
115 | | - return { |
116 | | - status: "ALLOW_PARTIAL", |
117 | | - rangesToRead: rangesThatStillNeedToBeRead, |
| 116 | + } catch (error) { |
| 117 | + // If we can't get file stats, it's safer to allow the read. |
| 118 | + if (error.code === "ENOENT") { |
| 119 | + // File doesn't exist, let the regular tool handle it. |
| 120 | + return { status: "ALLOW_ALL" } |
| 121 | + } |
| 122 | + console.error(`Error processing file read request for ${requestedFilePath}:`, error) |
| 123 | + // On other errors, allow the read to proceed to handle it. |
| 124 | + return { status: "ALLOW_ALL" } |
118 | 125 | } |
119 | 126 | } |
0 commit comments