Skip to content

Commit 52e8688

Browse files
committed
fix(grep): stream ripgrep output to prevent memory exhaustion
1 parent f9d5e18 commit 52e8688

1 file changed

Lines changed: 77 additions & 32 deletions

File tree

packages/opencode/src/tool/grep.ts

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import DESCRIPTION from "./grep.txt"
66
import { Instance } from "../project/instance"
77

88
const MAX_LINE_LENGTH = 2000
9+
const MATCH_LIMIT = 100
910

1011
export const GrepTool = Tool.define("grep", {
1112
description: DESCRIPTION,
@@ -33,51 +34,95 @@ export const GrepTool = Tool.define("grep", {
3334
stderr: "pipe",
3435
})
3536

36-
const output = await new Response(proc.stdout).text()
37+
const reader = proc.stdout.getReader()
38+
const decoder = new TextDecoder()
39+
let buffer = ""
40+
const matches: Array<{
41+
path: string
42+
modTime: number
43+
lineNum: number
44+
lineText: string
45+
}> = []
46+
let hitCollectLimit = false
47+
48+
try {
49+
while (true) {
50+
const { done, value } = await reader.read()
51+
if (done) break
52+
53+
buffer += decoder.decode(value, { stream: true })
54+
const lines = buffer.split("\n")
55+
buffer = lines.pop() || ""
56+
57+
for (const line of lines) {
58+
if (!line) continue
59+
if (matches.length >= MATCH_LIMIT) {
60+
hitCollectLimit = true
61+
break
62+
}
63+
64+
const [filePath, lineNumStr, ...lineTextParts] = line.split("|")
65+
if (!filePath || !lineNumStr || lineTextParts.length === 0) continue
66+
67+
const lineNum = parseInt(lineNumStr, 10)
68+
const lineText = lineTextParts.join("|")
69+
70+
const file = Bun.file(filePath)
71+
const stats = await file.stat().catch(() => null)
72+
if (!stats) continue
73+
74+
matches.push({
75+
path: filePath,
76+
modTime: stats.mtime.getTime(),
77+
lineNum,
78+
lineText,
79+
})
80+
}
81+
82+
if (hitCollectLimit) break
83+
}
84+
85+
if (!hitCollectLimit && buffer) {
86+
const [filePath, lineNumStr, ...lineTextParts] = buffer.split("|")
87+
if (filePath && lineNumStr && lineTextParts.length > 0) {
88+
const lineNum = parseInt(lineNumStr, 10)
89+
const lineText = lineTextParts.join("|")
90+
const file = Bun.file(filePath)
91+
const stats = await file.stat().catch(() => null)
92+
if (stats) {
93+
matches.push({
94+
path: filePath,
95+
modTime: stats.mtime.getTime(),
96+
lineNum,
97+
lineText,
98+
})
99+
}
100+
}
101+
}
102+
} finally {
103+
if (hitCollectLimit) proc.kill()
104+
reader.releaseLock()
105+
}
106+
37107
const errorOutput = await new Response(proc.stderr).text()
38108
const exitCode = await proc.exited
39109

40-
if (exitCode === 1) {
110+
if (exitCode === 1 && matches.length === 0) {
41111
return {
42112
title: params.pattern,
43113
metadata: { matches: 0, truncated: false },
44114
output: "No files found",
45115
}
46116
}
47117

48-
if (exitCode !== 0) {
118+
if (exitCode !== 0 && exitCode !== 1 && !hitCollectLimit) {
49119
throw new Error(`ripgrep failed: ${errorOutput}`)
50120
}
51121

52-
const lines = output.trim().split("\n")
53-
const matches = []
54-
55-
for (const line of lines) {
56-
if (!line) continue
57-
58-
const [filePath, lineNumStr, ...lineTextParts] = line.split("|")
59-
if (!filePath || !lineNumStr || lineTextParts.length === 0) continue
60-
61-
const lineNum = parseInt(lineNumStr, 10)
62-
const lineText = lineTextParts.join("|")
63-
64-
const file = Bun.file(filePath)
65-
const stats = await file.stat().catch(() => null)
66-
if (!stats) continue
67-
68-
matches.push({
69-
path: filePath,
70-
modTime: stats.mtime.getTime(),
71-
lineNum,
72-
lineText,
73-
})
74-
}
75-
76122
matches.sort((a, b) => b.modTime - a.modTime)
77123

78-
const limit = 100
79-
const truncated = matches.length > limit
80-
const finalMatches = truncated ? matches.slice(0, limit) : matches
124+
const truncated = matches.length > MATCH_LIMIT
125+
const finalMatches = truncated ? matches.slice(0, MATCH_LIMIT) : matches
81126

82127
if (finalMatches.length === 0) {
83128
return {
@@ -103,7 +148,7 @@ export const GrepTool = Tool.define("grep", {
103148
outputLines.push(` Line ${match.lineNum}: ${truncatedLineText}`)
104149
}
105150

106-
if (truncated) {
151+
if (truncated || hitCollectLimit) {
107152
outputLines.push("")
108153
outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)")
109154
}
@@ -112,7 +157,7 @@ export const GrepTool = Tool.define("grep", {
112157
title: params.pattern,
113158
metadata: {
114159
matches: finalMatches.length,
115-
truncated,
160+
truncated: truncated || hitCollectLimit,
116161
},
117162
output: outputLines.join("\n"),
118163
}

0 commit comments

Comments
 (0)