From 3f2817d6c306c024a7200f36e087dd15c97bcad8 Mon Sep 17 00:00:00 2001 From: jeffusion Date: Wed, 4 Mar 2026 17:29:02 +0800 Subject: [PATCH] fix(config): make persistOverrides resilient to read-only filesystems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Atomic rename (temp→target) fails on K8s volumes with EBUSY/EXDEV/EROFS. Fall back to direct writeFile when rename fails, with best-effort cleanup of orphaned temp files. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) --- src/config/config-manager.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/config/config-manager.ts b/src/config/config-manager.ts index 28445df..01c6fa7 100644 --- a/src/config/config-manager.ts +++ b/src/config/config-manager.ts @@ -10,7 +10,7 @@ import { randomUUID } from 'node:crypto'; import { readFileSync } from 'node:fs'; -import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { config as dotenvConfig } from 'dotenv'; import { z } from 'zod'; @@ -204,7 +204,7 @@ class ConfigManager { } } - /** Persist current overrides to disk atomically (write temp → rename). */ + /** Persist current overrides to disk. Tries atomic rename; falls back to direct write. */ private async persistOverrides(): Promise { const dir = dirname(this.overridesPath); await mkdir(dir, { recursive: true }); @@ -215,9 +215,18 @@ class ConfigManager { overrides: { ...this.overrides }, }; + const json = JSON.stringify(payload, null, 2); + + // Atomic rename may fail on K8s volumes (EBUSY/EXDEV); fall back to direct write. const tmpPath = `${this.overridesPath}.${randomUUID()}.tmp`; - await writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8'); - await rename(tmpPath, this.overridesPath); + try { + await writeFile(tmpPath, json, 'utf-8'); + await rename(tmpPath, this.overridesPath); + } catch { + await writeFile(this.overridesPath, json, 'utf-8'); + // Clean up orphaned tmp file (best effort) + try { await unlink(tmpPath); } catch { /* ignore */ } + } } // ── Core API ─────────────────────────────────────────────────────────────