From 8ea526a4c90ee074d07b495e4aae1ad27f24f2c4 Mon Sep 17 00:00:00 2001 From: Bin Date: Fri, 12 Jun 2026 10:09:48 +0800 Subject: [PATCH 1/2] fix: restore terminal state on abnormal exit Add signal handlers for SIGINT, SIGTERM, and beforeExit to ensure terminal cleanup (raw mode, alternate screen buffer) happens even when the process is killed by model errors or external signals. Fixes: Terminal scrolling persists after CTRL+C exit, and session resume crashes due to corrupted terminal state. --- packages/opencode/src/cli/cmd/tui/context/exit.tsx | 3 +++ packages/opencode/src/cli/cmd/tui/thread.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 9724726f..0a7766a1 100644 --- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx @@ -60,6 +60,9 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ }, ) process.on("SIGHUP", () => exit()) + process.on("SIGINT", () => exit()) + process.on("SIGTERM", () => exit()) + process.on("beforeExit", () => exit()) return exit }, }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 487554ab..72b7690f 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -168,6 +168,8 @@ export const TuiThreadCommand = cmd({ process.on("uncaughtException", error) process.on("unhandledRejection", error) process.on("SIGUSR2", reload) + process.on("SIGINT", () => stop()) + process.on("SIGTERM", () => stop()) let stopped = false const stop = async () => { From 24b878b35103c9d83cab7fce85018d43c4330421 Mon Sep 17 00:00:00 2001 From: Bin Date: Fri, 12 Jun 2026 12:16:15 +0800 Subject: [PATCH 2/2] fix: startup crash in git repos (EEXIST) and unbounded log file growth Sync from upstream PR #274 (whitelonng): - setupProjectIdEnvironment: tolerate filesystem errors on Windows - Log rotation: cap at 20MB per file, keep 10 files max (~200MB total) - Update cleanup glob pattern to match rotated filenames Fixes #243, #209, #219 --- packages/opencode/src/project/project.ts | 8 +++++--- packages/opencode/src/util/log.ts | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 1970afdb..4fc609ce 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -19,6 +19,8 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" +// Best-effort: never let project-id bookkeeping crash startup (e.g. EEXIST/EPERM +// from mkdir on Windows network drives or read-only .git dirs). async function setupProjectIdEnvironment(workingDir: string): Promise { const mainGit = resolveMainGitDir(workingDir) if (!mainGit) return @@ -29,19 +31,19 @@ async function setupProjectIdEnvironment(workingDir: string): Promise { if (await Bun.file(localFile).exists()) { if (!(await Bun.file(idFile).exists())) { const id = await Bun.file(localFile).text() - await Bun.write(idFile, id) + await Bun.write(idFile, id).catch(() => {}) } await nodeFs.unlink(localFile).catch(() => {}) } // Belt-and-suspenders: ensure .git/info/exclude lists .mimocode-project-id const excludeFile = nodePath.join(mainGit, "info", "exclude") - await nodeFs.mkdir(nodePath.dirname(excludeFile), { recursive: true }) + await nodeFs.mkdir(nodePath.dirname(excludeFile), { recursive: true }).catch(() => {}) const existing = await Bun.file(excludeFile) .text() .catch(() => "") if (!existing.includes(".mimocode-project-id")) { - await nodeFs.appendFile(excludeFile, "\n.mimocode-project-id\n") + await nodeFs.appendFile(excludeFile, "\n.mimocode-project-id\n").catch(() => {}) } } diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 09675e27..44bd9721 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -15,6 +15,9 @@ const levelPriority: Record = { ERROR: 3, } const keep = 10 +// Cap individual log files so runaway logging can't fill the disk; with `keep` +// pruning this bounds the log directory to roughly keep * maxFileSize. +const maxFileSize = 20 * 1024 * 1024 let level: Level = "INFO" @@ -79,7 +82,21 @@ export async function init(options: Options) { await fs.truncate(logpath).catch(() => {}) } const stream = createWriteStream(logpath, { flags: "a" }) + let written = (await fs.stat(logpath).catch(() => null))?.size ?? 0 + let rotations = 0 + const rotate = () => { + stream.end() + rotations++ + const stamp = new Date().toISOString().split(".")[0].replace(/:/g, "") + // Counter suffix keeps the name unique even when rotating within a second + logpath = path.join(Global.Path.log, `${stamp}_${rotations}.log`) + stream = createWriteStream(logpath, { flags: "a" }) + written = 0 + void cleanup(Global.Path.log) + } write = async (msg: any) => { + if (!options.dev && written >= maxFileSize) rotate() + written += Buffer.byteLength(msg) return new Promise((resolve, reject) => { stream.write(msg, (err) => { if (err) reject(err) @@ -91,7 +108,7 @@ export async function init(options: Options) { async function cleanup(dir: string) { const files = ( - await Glob.scan("????-??-??T??????.log", { + await Glob.scan("????-??-??T??????*.log", { cwd: dir, absolute: false, include: "file",