Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/opencode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ export namespace LSPClient {
},
async shutdown() {
l.info("shutting down")
diagnostics.clear()
for (const key of Object.keys(files)) delete files[key]
connection.end()
connection.dispose()
input.server.process.kill()
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export namespace LSP {
},
async (state) => {
await Promise.all(state.clients.map((client) => client.shutdown()))
state.clients.length = 0
state.broken.clear()
state.spawning.clear()
},
)

Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,17 @@ export namespace Plugin {
return state().then((x) => x.hooks)
}

let unsub: (() => void) | undefined

export async function init() {
const hooks = await state().then((x) => x.hooks)
const config = await Config.get()
for (const hook of hooks) {
// @ts-expect-error this is because we haven't moved plugin to sdk v2
await hook.config?.(config)
}
Bus.subscribeAll(async (input) => {
unsub?.()
unsub = Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
hook["event"]?.({
Expand Down
15 changes: 13 additions & 2 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export namespace SessionProcessor {
needsCompaction = false
const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true
while (true) {
let currentText: MessageV2.TextPart | undefined
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
try {
let currentText: MessageV2.TextPart | undefined
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
const stream = await LLM.stream(streamInput)

for await (const value of stream.fullStream) {
Expand Down Expand Up @@ -285,6 +285,9 @@ export namespace SessionProcessor {
) {
needsCompaction = true
}
// Incremental GC between steps to curb virtual memory growth
// during multi-tool-call sequences.
Bun.gc(false)
break

case "text-start":
Expand Down Expand Up @@ -374,6 +377,9 @@ export namespace SessionProcessor {
next: Date.now() + delay,
})
await SessionRetry.sleep(delay, input.abort).catch(() => {})
// Release stream references before retry
currentText = undefined
for (const key of Object.keys(reasoningMap)) delete reasoningMap[key]
continue
}
input.assistantMessage.error = error
Expand All @@ -384,6 +390,9 @@ export namespace SessionProcessor {
SessionStatus.set(input.sessionID, { type: "idle" })
}
}
// Release stream-scoped references
currentText = undefined
for (const key of Object.keys(reasoningMap)) delete reasoningMap[key]
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
if (patch.files.length) {
Expand Down Expand Up @@ -417,6 +426,8 @@ export namespace SessionProcessor {
}
input.assistantMessage.time.completed = Date.now()
await Session.updateMessage(input.assistantMessage)
// Release tool call references to allow GC of part data
for (const key of Object.keys(toolcalls)) delete toolcalls[key]
if (needsCompaction) return "compact"
if (blocked) return "stop"
if (input.assistantMessage.error) return "stop"
Expand Down
10 changes: 10 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,12 @@ export namespace SessionPrompt {
toolChoice: format.type === "json_schema" ? "required" : undefined,
})

// Release conversation data and hint GC to reclaim memory.
// Bun/JSC reserves large virtual memory regions that accumulate
// without explicit collection, eventually triggering the OOM killer.
msgs = [] as any
Bun.gc(true)

// If structured output was captured, save it and exit immediately
// This takes priority because the StructuredOutput tool was called successfully
if (structuredOutput !== undefined) {
Expand Down Expand Up @@ -714,6 +720,10 @@ export namespace SessionPrompt {
continue
}
SessionCompaction.prune({ sessionID })

// Final GC pass to reclaim memory from the entire processing loop
Bun.gc(true)

for await (const item of MessageV2.stream(sessionID)) {
if (item.info.role === "user") continue
const queued = state()[sessionID]?.callbacks ?? []
Expand Down
Loading