From e7673abeb1d9a93d35a9fc9248ed36b8f92b070a Mon Sep 17 00:00:00 2001 From: Javier Ardila Date: Sat, 25 Apr 2026 15:12:30 +0200 Subject: [PATCH] fix(opencode): resolve heap unlimited + orphan processes on Linux - Add SIGTERM handler in exit.tsx alongside existing SIGHUP handler - Add SIGTERM handler in thread.ts to gracefully stop worker - Add Effect.ensuring to abort AbortController in prompt.ts execRead - Replace unbounded Map with LRU cache in instance.ts (max 20 entries) Fixes #15348 This addresses the critical issue where: 1. Processes spawned by opencode on Linux become orphaned when terminal closes 2. AbortController instances accumulate in session prompts causing heap growth 3. Instance cache grows without bound holding references to contexts Closes #15348 --- packages/opencode/src/cli/cmd/tui/context/exit.tsx | 1 + packages/opencode/src/cli/cmd/tui/thread.ts | 2 ++ packages/opencode/src/project/instance.ts | 5 ++++- packages/opencode/src/session/prompt.ts | 5 ++++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 205025f867de..74ef88265787 100644 --- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx @@ -55,6 +55,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ }, ) process.on("SIGHUP", () => exit()) + process.on("SIGTERM", () => exit()) return exit }, }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index a2a53ecafa0d..ed6e585ea8ee 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -163,6 +163,7 @@ export const TuiThreadCommand = cmd({ process.on("uncaughtException", error) process.on("unhandledRejection", error) process.on("SIGUSR2", reload) + process.on("SIGTERM", () => stop()) let stopped = false const stop = async () => { @@ -171,6 +172,7 @@ export const TuiThreadCommand = cmd({ process.off("uncaughtException", error) process.off("unhandledRejection", error) process.off("SIGUSR2", reload) + process.off("SIGTERM", stop) await withTimeout(client.call("shutdown", undefined), 5000).catch((error) => { Log.Default.warn("worker shutdown failed", { error: errorMessage(error), diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 1c5109620467..e83854371deb 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -4,6 +4,7 @@ import { makeRuntime } from "@/effect/run-service" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { iife } from "@/util/iife" import { Log } from "@/util" +import { createLruCache } from "@/util/cache" import { LocalContext } from "../util" import * as Project from "./project" import { WorkspaceContext } from "@/control-plane/workspace-context" @@ -15,7 +16,9 @@ export interface InstanceContext { } const context = LocalContext.create("instance") -const cache = new Map>() +const cache = createLruCache>({ + maxEntries: 20, +}) const project = makeRuntime(Project.Service, Project.defaultLayer) const disposal = { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5f3530bcefa7..d65c2b2aeb6e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1062,7 +1062,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the metadata: () => Effect.void, ask: () => Effect.void, }) - .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort()))) + .pipe( + Effect.onInterrupt(() => Effect.sync(() => controller.abort())), + Effect.ensuring(Effect.sync(() => controller.abort())), + ) } if (part.mime === "text/plain") {