Skip to content

Commit a7c4e44

Browse files
committed
fix: remove dead LSP clients to prevent unbounded memory growth
When an LSP connection dies, the dead client stays in the active list forever. Every subsequent file write/edit retries it, waits 3s for diagnostics, and leaks memory. Over days this compounds to GB-scale growth (observed: 16.9 GB from 807 retries across 129 files). - Register connection.onClose() to detect dead connections proactively - Remove dead clients from s.clients and add to s.broken on first failure - Per-client error handling in touchFile so one dead client doesn't affect others - Add write tool guidance to avoid rapid successive writes Relates to #13796, #15675, #16697
1 parent 9c00669 commit a7c4e44

3 files changed

Lines changed: 44 additions & 7 deletions

File tree

packages/opencode/src/lsp/client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ export namespace LSPClient {
7676
uri: pathToFileURL(input.root).href,
7777
},
7878
])
79+
let alive = true
80+
connection.onClose(() => {
81+
l.info("connection closed")
82+
alive = false
83+
})
84+
connection.onError((err) => {
85+
l.error("connection error", { error: err })
86+
})
87+
7988
connection.listen()
8089

8190
l.info("sending initialize")
@@ -141,6 +150,9 @@ export namespace LSPClient {
141150
get serverID() {
142151
return input.serverID
143152
},
153+
get alive() {
154+
return alive
155+
},
144156
get connection() {
145157
return connection
146158
},

packages/opencode/src/lsp/index.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -275,18 +275,42 @@ export namespace LSP {
275275
return false
276276
}
277277

278+
async function removeClient(s: Awaited<ReturnType<typeof state>>, client: LSPClient.Info) {
279+
const idx = s.clients.indexOf(client)
280+
if (idx !== -1) s.clients.splice(idx, 1)
281+
s.broken.add(client.root + client.serverID)
282+
client.shutdown().catch(() => {})
283+
log.info("removed dead LSP client", {
284+
serverID: client.serverID,
285+
root: client.root,
286+
})
287+
}
288+
278289
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
279290
log.info("touching file", { file: input })
291+
const s = await state()
280292
const clients = await getClients(input)
281293
await Promise.all(
282294
clients.map(async (client) => {
283-
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
284-
await client.notify.open({ path: input })
285-
return wait
295+
if (!client.alive) {
296+
await removeClient(s, client)
297+
return
298+
}
299+
try {
300+
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
301+
await client.notify.open({ path: input })
302+
return wait
303+
} catch (err: any) {
304+
log.error("failed to touch file", { err, file: input })
305+
if (
306+
err?.message?.includes("Connection is closed") ||
307+
err?.message?.includes("connection is disposed")
308+
) {
309+
await removeClient(s, client)
310+
}
311+
}
286312
}),
287-
).catch((err) => {
288-
log.error("failed to touch file", { err, file: input })
289-
})
313+
)
290314
}
291315

292316
export async function diagnostics() {

packages/opencode/src/tool/write.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Writes a file to the local filesystem.
33
Usage:
44
- This tool will overwrite the existing file if there is one at the provided path.
55
- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
6-
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
6+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. Use the Edit tool instead of Write when only a portion of the file changed.
7+
- When creating or writing multiple files, avoid writing more than 5 files in rapid succession. Each write triggers formatting, LSP, and snapshot operations that need time to complete.
78
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
89
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.

0 commit comments

Comments
 (0)