Skip to content

fix: fsync state file before rename to survive crashes - #23

Open
ming-14 wants to merge 1 commit into
prevalentWare:mainfrom
ming-14:fix/fsync-state-writes
Open

fix: fsync state file before rename to survive crashes#23
ming-14 wants to merge 1 commit into
prevalentWare:mainfrom
ming-14:fix/fsync-state-writes

Conversation

@ming-14

@ming-14 ming-14 commented Aug 13, 2026

Copy link
Copy Markdown

Problem

If the opencode process terminates unexpectedly (crash, power loss, system restart), every subsequent prompt fails with:

Failed to send prompt / Unexpected server error

Server log shows:

StateDecodeError: JSON Parse error: Unrecognized token '\u0000'

Root cause: the state file goals.json (%APPDATA%/opencode-goal-plugin/goals.json) was found zero-filled — correct file size but all bytes \u0000.

Why

writeStateEffect (src/state.ts) wrote to a temp file and immediately renamed it into place without an intervening fsync:

  1. writeFile(tmp, JSON.stringify(state, null, 2) + "\n")
  2. rename(tmp, file)

On crash, the rename metadata could be committed while the data blocks were still in the OS page cache and never flushed to disk, leaving a zero-filled file after restart. A leftover .tmp file with intact contents further confirms the write was interrupted.

Fix

fsync the temp file before renaming it into place:

const handle = await open(tmp, "w", 0o600)
try {
  await handle.writeFile(JSON.stringify(state, null, 2) + "\n")
  await handle.sync()   // flush to disk before rename
} finally {
  await handle.close()
}
await rename(tmp, file)

Validation

  • bun run typecheck / bun run lint / bun run build pass
  • bun test: 93 pass; the only failing test (writes state with owner-only file permissions) is a pre-existing Windows chmod semantics issue, failing before this change
  • 3 timer-based server tests fail intermittently and pass on re-run; unrelated to this change

AI Assistance: This pull request was created with the assistance of AI (opencode using deepseek-v4).

Write the goal state to a temp file and fsync it before renaming into
place. Previously the temp file was never flushed, so a crash (power
loss / BSOD) could leave an empty or partially-written state file that
the plugin then failed to decode on every prompt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant