appendRecord wrote token-usage.json in place with a direct fs.writeFile
call; a crash or SIGKILL during that write left truncated JSON. Because
readJsonArray now throws on any non-ENOENT error (to prevent silent data
loss) and recordTokenUsage callers swallow the error via .catch(), one
corrupted write permanently disabled all future token logging until the
file was manually repaired.
The in-place-write bug was fixed in 8c162d0ba via a temp-file + atomic
rename approach, but usage-log.ts still carried its own private
withFileLock / isLockStale implementation. That inline lock had two
known bugs that were fixed in plugin-sdk/file-lock.ts but never applied
here:
1. isLockStale treated empty / unparseable lock content as 'not stale'
— a process that crashes between open('wx') and writeFile(pid)
leaves an empty .lock that appeared live forever, blocking all
future writers until it was manually removed.
2. No inode identity check before unlink: two waiters observing the
same stale lock could both call unlink; the slower one would
delete the faster one's freshly-acquired lock, letting both enter
fn() concurrently and race on the read-modify-write sequence.
Fix: import withFileLock from infra/file-lock.ts (which re-exports the
canonical plugin-sdk implementation) and remove the ~70-line inline lock.
APPEND_LOCK_OPTIONS reproduces the previous timeout/retry budget
(~100 × 50 ms ≈ 5 s) while gaining all fixes from plugin-sdk/file-lock.
The lock payload format changed from a plain PID string to the JSON
{pid, createdAt} envelope expected by the shared implementation; the
stale-lock integration test is updated to match.