Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit a643b55

Browse files
committed
fix: additional memory leak fixes from upstream PR anomalyco#10914
- AsyncQueue: add close()/drain() methods, iterator exits on close - Bash tool: use Buffer[] ring buffer with 10MB cap instead of unbounded concat - LSP client: clear diagnostics map on shutdown, cap at 5000 entries - Bus: clear subscriptions map on instance dispose - PTY: use Buffer[] instead of string concat for session buffer
1 parent 29ba566 commit a643b55

5 files changed

Lines changed: 76 additions & 27 deletions

File tree

packages/opencode/src/bus/index.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,18 @@ export namespace Bus {
2525
},
2626
async (entry) => {
2727
const wildcard = entry.subscriptions.get("*")
28-
if (!wildcard) return
29-
const event = {
30-
type: InstanceDisposed.type,
31-
properties: {
32-
directory: Instance.directory,
33-
},
34-
}
35-
for (const sub of [...wildcard]) {
36-
sub(event)
28+
if (wildcard) {
29+
const event = {
30+
type: InstanceDisposed.type,
31+
properties: {
32+
directory: Instance.directory,
33+
},
34+
}
35+
for (const sub of [...wildcard]) {
36+
sub(event)
37+
}
3738
}
39+
entry.subscriptions.clear()
3840
},
3941
)
4042

packages/opencode/src/lsp/client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export namespace LSPClient {
4848
new StreamMessageWriter(input.server.process.stdin as any),
4949
)
5050

51+
const MAX_DIAGNOSTICS_FILES = 5000
5152
const diagnostics = new Map<string, Diagnostic[]>()
5253
connection.onNotification("textDocument/publishDiagnostics", (params) => {
5354
const filePath = Filesystem.normalizePath(fileURLToPath(params.uri))
@@ -56,6 +57,11 @@ export namespace LSPClient {
5657
count: params.diagnostics.length,
5758
})
5859
const exists = diagnostics.has(filePath)
60+
// Evict oldest entries if map grows too large
61+
if (!exists && diagnostics.size >= MAX_DIAGNOSTICS_FILES) {
62+
const oldest = diagnostics.keys().next().value
63+
if (oldest !== undefined) diagnostics.delete(oldest)
64+
}
5965
diagnostics.set(filePath, params.diagnostics)
6066
if (!exists && input.serverID === "typescript") return
6167
Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
@@ -240,6 +246,7 @@ export namespace LSPClient {
240246
files.clear()
241247
connection.end()
242248
connection.dispose()
249+
diagnostics.clear()
243250
input.server.process.kill()
244251
l.info("shutdown")
245252
},

packages/opencode/src/pty/index.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ export namespace Pty {
6767
interface ActiveSession {
6868
info: Info
6969
process: IPty
70-
buffer: string
70+
bufferChunks: Buffer[]
71+
bufferSize: number
7172
subscribers: Set<WSContext>
7273
}
7374

@@ -138,7 +139,8 @@ export namespace Pty {
138139
const session: ActiveSession = {
139140
info,
140141
process: ptyProcess,
141-
buffer: "",
142+
bufferChunks: [],
143+
bufferSize: 0,
142144
subscribers: new Set(),
143145
}
144146
state().set(id, session)
@@ -153,9 +155,14 @@ export namespace Pty {
153155
ws.send(data)
154156
}
155157
if (open) return
156-
session.buffer += data
157-
if (session.buffer.length <= BUFFER_LIMIT) return
158-
session.buffer = session.buffer.slice(-BUFFER_LIMIT)
158+
const chunk = Buffer.from(data)
159+
session.bufferChunks.push(chunk)
160+
session.bufferSize += chunk.length
161+
// Trim oldest chunks when exceeding limit
162+
while (session.bufferSize > BUFFER_LIMIT && session.bufferChunks.length > 1) {
163+
const dropped = session.bufferChunks.shift()!
164+
session.bufferSize -= dropped.length
165+
}
159166
})
160167
ptyProcess.onExit(({ exitCode }) => {
161168
log.info("session exited", { id, exitCode })
@@ -223,16 +230,19 @@ export namespace Pty {
223230
}
224231
log.info("client connected to session", { id })
225232
session.subscribers.add(ws)
226-
if (session.buffer) {
227-
const buffer = session.buffer.length <= BUFFER_LIMIT ? session.buffer : session.buffer.slice(-BUFFER_LIMIT)
228-
session.buffer = ""
233+
if (session.bufferChunks.length > 0) {
234+
const buffer = Buffer.concat(session.bufferChunks).toString()
235+
session.bufferChunks.length = 0
236+
session.bufferSize = 0
229237
try {
230238
for (let i = 0; i < buffer.length; i += BUFFER_CHUNK) {
231239
ws.send(buffer.slice(i, i + BUFFER_CHUNK))
232240
}
233241
} catch {
234242
session.subscribers.delete(ws)
235-
session.buffer = buffer
243+
const chunk = Buffer.from(buffer)
244+
session.bufferChunks.push(chunk)
245+
session.bufferSize = chunk.length
236246
ws.close()
237247
return
238248
}

packages/opencode/src/tool/bash.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ export const BashTool = Tool.define("bash", async () => {
175175
detached: process.platform !== "win32",
176176
})
177177

178-
const chunks: Buffer[] = []
178+
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024 // 10MB
179+
const outputChunks: Buffer[] = []
180+
let outputSize = 0
179181

180182
// Initialize metadata with empty output
181183
ctx.metadata({
@@ -186,12 +188,17 @@ export const BashTool = Tool.define("bash", async () => {
186188
})
187189

188190
const append = (chunk: Buffer) => {
189-
chunks.push(chunk)
190-
const output = Buffer.concat(chunks).toString()
191+
outputChunks.push(chunk)
192+
outputSize += chunk.length
193+
// Ring buffer: drop earliest chunks when exceeding limit
194+
while (outputSize > MAX_OUTPUT_BYTES && outputChunks.length > 1) {
195+
const dropped = outputChunks.shift()!
196+
outputSize -= dropped.length
197+
}
198+
const preview = Buffer.concat(outputChunks).toString("utf-8")
191199
ctx.metadata({
192200
metadata: {
193-
// truncate the metadata to avoid GIANT blobs of data (has nothing to do w/ what agent can access)
194-
output: output.length > MAX_METADATA_LENGTH ? output.slice(0, MAX_METADATA_LENGTH) + "\n\n..." : output,
201+
output: preview.length > MAX_METADATA_LENGTH ? preview.slice(0, MAX_METADATA_LENGTH) + "\n\n..." : preview,
195202
description: params.description,
196203
},
197204
})
@@ -242,6 +249,10 @@ export const BashTool = Tool.define("bash", async () => {
242249
})
243250
})
244251

252+
let output = Buffer.concat(outputChunks).toString("utf-8")
253+
outputChunks.length = 0
254+
outputSize = 0
255+
245256
const resultMetadata: string[] = []
246257

247258
if (timedOut) {
@@ -252,7 +263,6 @@ export const BashTool = Tool.define("bash", async () => {
252263
resultMetadata.push("User aborted the command")
253264
}
254265

255-
let output = Buffer.concat(chunks).toString()
256266
if (resultMetadata.length > 0) {
257267
output += "\n\n<bash_metadata>\n" + resultMetadata.join("\n") + "\n</bash_metadata>"
258268
}

packages/opencode/src/util/queue.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
11
export class AsyncQueue<T> implements AsyncIterable<T> {
22
private queue: T[] = []
3-
private resolvers: ((value: T) => void)[] = []
3+
private resolvers: ((value: T | undefined) => void)[] = []
4+
private closed = false
45

56
push(item: T) {
7+
if (this.closed) return
68
const resolve = this.resolvers.shift()
79
if (resolve) resolve(item)
810
else this.queue.push(item)
911
}
1012

11-
async next(): Promise<T> {
13+
async next(): Promise<T | undefined> {
1214
if (this.queue.length > 0) return this.queue.shift()!
15+
if (this.closed) return undefined
1316
return new Promise((resolve) => this.resolvers.push(resolve))
1417
}
1518

19+
close() {
20+
this.closed = true
21+
for (const resolve of this.resolvers) {
22+
resolve(undefined)
23+
}
24+
this.resolvers.length = 0
25+
}
26+
27+
drain() {
28+
this.close()
29+
this.queue.length = 0
30+
}
31+
1632
async *[Symbol.asyncIterator]() {
17-
while (true) yield await this.next()
33+
while (true) {
34+
const value = await this.next()
35+
if (value === undefined) return
36+
yield value
37+
}
1838
}
1939
}
2040

0 commit comments

Comments
 (0)