-
Notifications
You must be signed in to change notification settings - Fork 13
Description
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 218Impact
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.