Skip to content

Uncaught Exception: Error: ENOSPC: no space left on device, o #61301

Open

Description

@ppk9058

pen '/Users/praveenkumar/Library/Application Support/ai.opencode.desktop/window-state-36ea1fd4-80b5-4837-9d61-a311aa582ad2.json.tmp'
at writeFileSync (node:fs:2482:20)
at BrowserWindow.c (file:///Applications/OpenCode.app/Contents/Resources/app.asar/out/main/index.js:1:54541)
at BrowserWindow.emit (node:events:526:24)

Activity

  1. opencodejr commented on Nov 2, 2026

    @opencodejr

    Thanks for the report. I was able to reproduce this.

    When a desktop window closes, the app saves its position and size to window-state-<id>.json in the app data folder. It writes a .tmp file first, then renames it over the real file. Nothing catches an error from that write, so when the disk is full the ENOSPC error escapes from the window's close event. Electron then shows it as the "Uncaught Exception" dialog you saw.

    I couldn't run the macOS desktop app here. Instead, the script below runs the same window-state code against a write target that always fails with "no space left on device" and fires the window's close events. It throws the same ENOSPC error from writeFileSync as in your trace, on v2.2.5 and on the latest dev (1bf08ae).

    The only thing lost is the saved window position and size. Freeing some disk space makes the dialog go away. It's related to #51470, where the same save fails with EPERM on Windows.

    Reproduction script (window-state-enospc.ts)
    // Reproduces #61301: the desktop window-state "closed" handler throws when the disk is full.
    // Run from the repo root of a dev checkout:  bun /workspace/repro/window-state-enospc.ts [/path/to/repo]
    // The `${file}.tmp` path is a symlink to /dev/full, so writeFileSync fails with a real ENOSPC (Linux).
    import { EventEmitter } from "node:events"
    import { mkdtempSync, symlinkSync, existsSync } from "node:fs"
    import { tmpdir } from "node:os"
    import path from "node:path"
    
    const repo = process.argv[2] ?? "/workspace/repo"
    const { manageWindowState, windowStateFile } = await import(
      path.join(repo, "packages/desktop/src/main/windows/window-state.ts")
    )
    
    // Minimal stand-in for Electron's BrowserWindow (an EventEmitter with the getters the module uses).
    class FakeWindow extends EventEmitter {
      isDestroyed = () => false
      getBounds = () => ({ x: 10, y: 10, width: 800, height: 600 })
      isMaximized = () => false
      isMinimized = () => false
      isFullScreen = () => false
      maximize() {}
      setFullScreen() {}
    }
    
    const display = { x: 0, y: 0, width: 1920, height: 1080 }
    const displays = { all: () => [display], primary: () => display, matching: () => display }
    
    const userData = mkdtempSync(path.join(tmpdir(), "ai.opencode.desktop-"))
    const file = path.join(userData, windowStateFile("36ea1fd4-80b5-4837-9d61-a311aa582ad2"))
    if (!existsSync("/dev/full")) throw new Error("needs /dev/full (Linux) to simulate a full disk")
    symlinkSync("/dev/full", `${file}.tmp`) // every write to window-state-*.json.tmp now gets ENOSPC
    
    const win = new FakeWindow()
    manageWindowState(win as any, file, { width: 800, height: 600 }, displays)
    win.emit("close")
    try {
      // Electron emits "closed" itself; an exception thrown by a listener there becomes
      // the "Uncaught Exception" dialog in the main process.
      win.emit("closed")
      console.log("OK: closed handler did not throw (bug not present)")
    } catch (err) {
      console.log("BUG REPRODUCED: 'closed' listener threw:")
      console.log(String((err as Error).stack).split("\n").slice(0, 4).join("\n"))
      process.exitCode = 1
    }

    Run it: bun window-state-enospc.ts

  2. Luxe7 commented on Nov 3, 2026

    @Luxe7

    I'd like to take this if nobody is working on it. After checking dev, I verified that a real write failure in a temporary directory escapes the closed callback and prevents later close listeners from running. My local case is EISDIR, not a full disk.

    I'll keep the fix to error handling around window-state saving, retain diagnostics, and add coverage for normal saves and continuing to close after a save failure. #54530 handles Windows lock retries on the same path; I won't duplicate its write or retry logic.

    Working on this with Codex assistance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions