Found during the PR2 (#295/#288) /code-review max sweep. Pre-existing — not introduced by that PR.
Problem
LexUserHistory::apply_records appends its commit-log lines after releasing the wal mutex:
} // wal guard dropped here
for line in &log_lines {
self.append_commit_log(line);
}
clear_impl serialises everything else behind compact_gate + the wal mutex, but it has no exclusion against a concurrent append_commit_log. It nulls the cached handle, releases the commit-log mutex, then unlinks the file — and CommitLog::append re-creates it with .create(true).append(true).
Interleaving
- Thread A commits a conversion, finishes the wal critical section, is descheduled before its commit-log append.
- The user clicks 履歴を全消去.
clear_impl writes the empty checkpoint, truncates the WAL, drops the log handle, remove_file("commit-log.jsonl") succeeds, returns Ok.
- Thread A runs
append_commit_log, sees file == None, reopens the path, and writes {"t":…,"reading":…,"surface":…,"rank":…}.
After a wipe the user was told succeeded, a JSONL file containing their raw input exists on disk. Unlike the WAL, the commit log has no startup scrub backstop — recovery never touches it (the comment in clear_impl says so explicitly).
Why this matters
CLAUDE.md 設計哲学: 「学習データはユーザーの資産: 破損・削除・復元をサイレントにしない」. A privacy wipe that silently leaves input strings behind is the same class as #295, on the diagnostic file instead of the history.
Reachability
Needs a commit in flight when clear runs — a narrow window (the gap between releasing the wal mutex and the append), but reachable with a background AsyncWorker commit landing as the user clicks the button.
Fix options
- Move the commit-log append inside the wal critical section. Correct and simple, but adds a file write to the key-processing thread's critical section, which the design deliberately kept out (SPEC: 「commit-log append(従来通り、wal ロック外、耐久化なし)」).
- Give
CommitLog a clear-generation: apply_records captures it under the wal lock alongside the lines, clear_impl bumps it, and append drops lines carrying a stale generation. Keeps the append off the critical section; costs one counter.
The second looks right, but it is a mechanism and belongs in its own change rather than bolted onto the durability channel.
Severity: medium (privacy). Files: engine/src/api/resources.rs (apply_records, clear_impl, CommitLog::append).
Found during the PR2 (#295/#288)
/code-review maxsweep. Pre-existing — not introduced by that PR.Problem
LexUserHistory::apply_recordsappends its commit-log lines after releasing the wal mutex:clear_implserialises everything else behindcompact_gate+ the wal mutex, but it has no exclusion against a concurrentappend_commit_log. It nulls the cached handle, releases the commit-log mutex, then unlinks the file — andCommitLog::appendre-creates it with.create(true).append(true).Interleaving
clear_implwrites the empty checkpoint, truncates the WAL, drops the log handle,remove_file("commit-log.jsonl")succeeds, returnsOk.append_commit_log, seesfile == None, reopens the path, and writes{"t":…,"reading":…,"surface":…,"rank":…}.After a wipe the user was told succeeded, a JSONL file containing their raw input exists on disk. Unlike the WAL, the commit log has no startup scrub backstop — recovery never touches it (the comment in
clear_implsays so explicitly).Why this matters
CLAUDE.md 設計哲学: 「学習データはユーザーの資産: 破損・削除・復元をサイレントにしない」. A privacy wipe that silently leaves input strings behind is the same class as #295, on the diagnostic file instead of the history.
Reachability
Needs a commit in flight when clear runs — a narrow window (the gap between releasing the wal mutex and the append), but reachable with a background
AsyncWorkercommit landing as the user clicks the button.Fix options
CommitLoga clear-generation:apply_recordscaptures it under the wal lock alongside the lines,clear_implbumps it, andappenddrops lines carrying a stale generation. Keeps the append off the critical section; costs one counter.The second looks right, but it is a mechanism and belongs in its own change rather than bolted onto the durability channel.
Severity: medium (privacy). Files:
engine/src/api/resources.rs(apply_records,clear_impl,CommitLog::append).