Skip to content

[Bug] StateStore silently discards state mutations that arrive during async write #21

@noahwaldner

Description

@noahwaldner

Severity: Fatal
Category: Data Loss
File: src/state-store.ts

Description

_doSaveAsync() sets this.dirty = false after the async write completes (line 218), not before serializing the state snapshot. Any state mutation that arrives while the file write is in progress correctly sets this.dirty = true — but that flag is then unconditionally overwritten to false when the write finishes. The debounced re-save that was scheduled for those mutations then fires, sees dirty = false, and skips the write. The mutations are permanently lost with no error or warning.

Event Loop Trace

1. _doSaveAsync() serializes state → json (snapshot at T1)
2. await writeFile(...)               ← suspends; libuv handles IO
3. [event loop] state mutation arrives:
     save() sets this.dirty = true
     save() schedules setTimeout(saveNowAsync, 500ms)
4. writeFile + rename complete
5. _doSaveAsync() sets this.dirty = false  ← OVERWRITES the true from step 3
6. _saveInFlight = null
7. 500ms later: debounced timer fires → saveNowAsync()
8. saveNowAsync() checks dirty → false → returns without saving

The mutation from step 3 is not on disk. No error is emitted.

Code

src/state-store.ts, _doSaveAsync():

// Step 1: Serialize state (snapshot taken here)
json = JSON.stringify(this.state);

// ... async write happens ...

// Step 3: sets dirty = false AFTER write — any mutations during write are erased
this.dirty = false;  // line 218

Impact

Session state written via save() (respawn config, task assignments, token counts, last-activity timestamps, Ralph Loop state) can be silently rolled back on every process restart. During a 24-hour autonomous run with high mutation frequency, the risk of a missed write is non-trivial. The backup file (state.json.bak) will also contain the stale snapshot, so the recovery path is equally affected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions