Skip to content

fix(work-events): clamp replay range to the stat-measured size#1462

Open
tjkang wants to merge 1 commit into
danielmiessler:mainfrom
tjkang:fix/work-events-replay-stat-race
Open

fix(work-events): clamp replay range to the stat-measured size#1462
tjkang wants to merge 1 commit into
danielmiessler:mainfrom
tjkang:fix/work-events-replay-stat-race

Conversation

@tjkang

@tjkang tjkang commented Jul 11, 2026

Copy link
Copy Markdown

PR A — fix(work-events): clamp replay range to the stat-measured size

Files: install/hooks/lib/work-events.ts, install/hooks/tests/work-events-stat-race.test.ts
Patch: patch-1-stat-race.diff

The bug

replayOntoSnapshot() measures the event-log size with statSync(), then reads
the whole file and replays events from offset to the end of the buffer
(buf.subarray(offset)), while returning the stat-measured size as the new
offset:

size = statSync(logPath).size;
...
const buf = readFileSync(logPath);
suffix = buf.subarray(offset).toString('utf-8');   // replays to buf.length
...
return { registry: foldEvents(...), offset: size }; // but reports the smaller size

Appends to the log happen outside the events lock, so the file can grow
between the stat() and the readFileSync(). When it does, an event written in
that window is replayed now but not covered by the returned offset. On the
next replay the same event is folded a second time — and if an offset-advance was
emitted in between, the double-apply can roll a field back to a stale value.

The fix (one line)

Clamp the replayed slice to the measured size so the replayed range and the
returned offset always agree:

suffix = buf.subarray(offset, Math.min(size, buf.length)).toString('utf-8');

Events in [size, buf.length) sit after the returned offset, so the next
read/fold picks them up exactly once. No API change.

Test

Adds a deterministic reproduction that mocks only statSync to simulate "the
file grew right after the stat":

  • against unpatched code it fails (folds the later event; phase = starting),
  • against the fix it passes (phase = execute, offset consistent).
cd install && bun test hooks/tests/work-events-stat-race.test.ts

Risk

Minimal — the read window only changes in the race case, where it becomes
correct. Outside the race, buf.length === size, so behavior is unchanged.


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