#839·openwiki

.last-update.json is written non-atomically; a failed or concurrent write can corrupt it

Author: Yashwanth-Kumar-KotlaCreated Sep 8, 2026Updated Sep 8, 2026

Bug description

writeLastUpdateMetadata in src/agent/utils.ts writes .last-update.json with a plain mkdir + writeFile, no temp-file-and-rename. A failed or concurrent write can leave the file truncated or torn, and readLastUpdate's catch (SyntaxError) → return null silently treats that as "no prior metadata," discarding whatever the file was actually recording.

This is the same class of bug already fixed for ~/.openwiki/.env in #407: writeFile opens the destination with O_TRUNC, truncating it to zero before writing the new content, so any failure mid-write (disk full, crash, power loss) leaves the file empty. .last-update.json was missed when that fix landed.

Why it matters

.last-update.json is exactly the file crash-guard.ts writes to on a fatal signal (stamping status: "interrupted"), and the file getUpdateNoopStatus reads to decide whether an update can skip re-running. Nothing in the repo locks this file, and OpenWiki's own CI-scheduling feature makes concurrent access realistic — a cron-triggered openwiki update overlapping a manual one, or the crash guard's interrupted-status write racing a normal completion write.

Every other file OpenWiki persists uses an atomic write already: src/integrations/install/atomic-file.ts's writeTextAtomic (temp file + rename), reused by src/generation/page-manifest.ts and src/generation/run-state.ts. writeLastUpdateMetadata is the one place still using a direct writeFile.

Steps to reproduce

Two concurrent writers (mirroring a scheduled update racing a manual one) plus a concurrent reader against the real exported writeLastUpdateMetadata, run 500 iterations each:

javascript
await Promise.all([
  writer("complete"),     // both call the real writeLastUpdateMetadata
  writer("interrupted"),
  reader(),                // readFile + JSON.parse on .last-update.json
]);

Result: 57% of reads got invalid/empty JSON — the concurrent writes tore the file mid-read.

A simpler, deterministic reproduction: seed .last-update.json with valid content, then make the write fail partway through (simulating ENOSPC — writeFile truncates the target, then throws). With the current implementation, the original content is destroyed even though the write never succeeded.

Expected behavior

A failed or concurrently-raced write should never corrupt the existing .last-update.json — either the old content or the new content should be readable, never a torn/empty file. This matches how every other persisted file in OpenWiki already behaves.

Suggested fix

Route the write through the existing writeTextAtomic helper (src/integrations/install/atomic-file.ts), the same pattern already used elsewhere in the codebase and already applied to ~/.openwiki/.env for the identical failure mode.