You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WriteToFileTool.execute() (src/core/tools/WriteToFileTool.ts:29) →
DiffViewProvider.saveDirectly() (src/integrations/editor/DiffViewProvider.ts:1141) = raw fs.writeFile(absolutePath, content): no lock, no version check, no atomic
publish, no fsync. This is the main agent write path.
EditFileTool (src/core/tools/EditFileTool.ts:139) validates old_string against a fresh
read at write time — a natural staleness detector for edits, but write_to_file (full
overwrite) has no staleness check at all.
ReadFileTool records no mtime/stat/checksum of the files it hands to the model, so no
compare-and-swap is possible anywhere in the codebase.
Auto-approval (src/core/auto-approval/) can skip user review entirely.
ExecuteCommandTool runs shell commands in the workspace; shell writes bypass any
filesystem guard (out of scope, below).
Internal state
safeWriteJson (src/utils/safeWriteJson.ts) is the JSON-state entry point (imported by
TaskHistoryStore, taskMessages, apiMessages, McpHub, webviewMessageHandler,
modelCache): proper-lockfile advisory lock, temp + rename, backup/rollback, merge
callback. Missing: fsync, Windows DACL preservation, and it does not cover the
workspace write path.
Parallel subtasks / new_task children: same process, shared workspacePath
(Task.ts L515-517), no file-level isolation.
Per-task isolation today = checkpoints shadow git at
/tasks//checkpoints (ShadowCheckpointService), triggered on
user message send — not on writes.
Cross-process (VS Code windows / JetBrains) coordination for workspace files: none.
Worktrees (packages/core/src/worktree/) are user-UI-only: prompts, tools, and
Task.startTask contain no worktree references, and git worktree add is a full checkout
of the base commit. The agent flow does not use it; it cannot serve as the isolation
mechanism for this work.
Solution
A — Safety core: version guard + per-path serialization + atomic publish
A1 Version token — dev:ino:size:mtimeNs:ctimeNs from one fs.stat (BigInt). Cost of
one stat; the token is a disk fact, so all processes observe the same value.
A2 Observation registry — per-task Map<absolutePath, version>, populated by
ReadFileTool (one extra stat per read). Owner = task, so parent and subtask
observations are independent.
A3 Guarded write and edit — compare-and-swap semantics on the write path:
unobserved target → createIfAbsent: succeeds for new files; if the file exists the
write fails, forcing a read first — the model re-reads and retries with the observed
version;
observed-absent → createIfAbsent;
observed-present → replaceIfVersion(version): a mismatch fails the tool call with
"stale version — re-read the file, then retry";
edit keeps the literal-match check plus the version guard; an unobserved edit
fails with "file not read yet — read the file, then retry".
Failures carry a remediation suffix the model can act on, so the standard loop
self-heals (re-read → retry); the user sees the failure as a step event in chat.
A per-absolute-path tail-promise chain (in-process FIFO) wraps read → guard →
publish, so concurrent subtask mutations to the same file are deterministically
ordered: one wins, the rest fail as stale.
Cross-process stance (explicit): no workspace lockfile. Two processes editing
the same file are detected via the version token; the loser fails as stale and
re-reads. A lockfile would block the user's own editor.
A4 Atomic publish — replace saveDirectly's raw fs.writeFile with: temp file in a
private per-write staging dir → fsync → rename; on Windows ReplaceFile (with DACL
copy), rename fallback. Generalize safeWriteJson's staging/backup/rollback into a
safeWriteText used by both file writes and JSON state.
default 0; keep the setting for users who want pacing
delay(300) before approval
WriteToFileTool.ts
remove
Blocking LSP diagnostics after save
saveDirectly tail
report asynchronously as a follow-up event
DiffView open/update/scroll choreography
default approval path
make the chat-diff (PREVENT_FOCUS_DISRUPTION) path the default
B — Per-step visibility + rollback
B1 Automatic checkpoint per file write — extend the existing checkpoints shadow
git so every successful write_to_file / edit_file / apply-patch records a checkpoint
(today only user message sends do). Task start becomes a real, O(1) baseline — not a
no-op, and not a worktree.
B2 Per-task change journal — append-only JSONL under the task dir (one entry per
file write: path, operation, checkpoint id, diff stats); torn-tail repair on load.
B3 Per-step change cards + rollback — per-step change cards in the existing chat
flow (reusing the unified diff + computeDiffStats already produced for approval) with
"N files changed this step", and rollback to any checkpoint per file or per step.
Auto-approval paths get the same cards after the fact — the user can always see and
undo what the agent did.
Acceptance criteria
No raw fs.writeFile / fs.appendFile in the workspace file-write path (all writes
through the A4 atomic-publish helper; JSON state through safeWriteJson).
Stale writes (file changed since this task's last read) fail loudly with the
re-read-then-retry remediation, and the agent recovers automatically in the standard
loop.
A write to an existing file the task never read fails (createIfAbsent conflict) and
the model recovers by reading first.
Every successful file write produces a rollback-able checkpoint and a journal entry;
the user can view each step's diff and roll back per step or per file.
Concurrent subtasks writing the same file: exactly one wins, the others receive stale
errors — deterministic, no corruption, no lost writes.
Problem
The agent's file-write path has no version guard, no atomic publish, and no per-step
visibility:
workspace, multiple VS Code windows are separate extension-host processes, and the
JetBrains plugin has confirmed real corruption under two IDE instances ([BUG][regression] Global _index.json full rewrite is unsafe under concurrent tasks (real corruption under JetBrains multi-agent) #1231).
task start creates no baseline at all.
overwritten on the next write.
safety.
Known issues
Current state (verified in code)
Agent write path
DiffViewProvider.saveDirectly() (src/integrations/editor/DiffViewProvider.ts:1141) =
raw fs.writeFile(absolutePath, content): no lock, no version check, no atomic
publish, no fsync. This is the main agent write path.
read at write time — a natural staleness detector for edits, but write_to_file (full
overwrite) has no staleness check at all.
compare-and-swap is possible anywhere in the codebase.
filesystem guard (out of scope, below).
Internal state
TaskHistoryStore, taskMessages, apiMessages, McpHub, webviewMessageHandler,
modelCache): proper-lockfile advisory lock, temp + rename, backup/rollback, merge
callback. Missing: fsync, Windows DACL preservation, and it does not cover the
workspace write path.
fileExistsAtPath() then a raw fs.writeFile of an empty stub — two windows both see
"absent" and the second blind write truncates the file to a 122-byte stub (TOCTOU).
and re-attach a severed subtask.
Concurrency
(Task.ts L515-517), no file-level isolation.
/tasks//checkpoints (ShadowCheckpointService), triggered on
user message send — not on writes.
Task.startTask contain no worktree references, and git worktree add is a full checkout
of the base commit. The agent flow does not use it; it cannot serve as the isolation
mechanism for this work.
Solution
A — Safety core: version guard + per-path serialization + atomic publish
one stat; the token is a disk fact, so all processes observe the same value.
ReadFileTool (one extra stat per read). Owner = task, so parent and subtask
observations are independent.
write fails, forcing a read first — the model re-reads and retries with the observed
version;
"stale version — re-read the file, then retry";
fails with "file not read yet — read the file, then retry".
self-heals (re-read → retry); the user sees the failure as a step event in chat.
publish, so concurrent subtask mutations to the same file are deterministically
ordered: one wins, the rest fail as stale.
the same file are detected via the version token; the loser fails as stale and
re-reads. A lockfile would block the user's own editor.
private per-write staging dir → fsync → rename; on Windows ReplaceFile (with DACL
copy), rename fallback. Generalize safeWriteJson's staging/backup/rollback into a
safeWriteText used by both file writes and JSON state.
safeWriteJson(..., { merge: keep existing if non-empty }) — atomic read-modify-write
under the advisory lock closes the TOCTOU.
feature/local-usage-stats, commit 1d1eb91).
locked-merge write.
finalization failure must not reuse the streaming-phase nativeArgs (partial-json
output); fail the tool call with an explicit "arguments were truncated" error instead
of writing the truncated content to disk.
Latency — net write-path cost added by A: one stat + one fsync + one rename. On top
of that, remove the artificial delays:
B — Per-step visibility + rollback
git so every successful write_to_file / edit_file / apply-patch records a checkpoint
(today only user message sends do). Task start becomes a real, O(1) baseline — not a
no-op, and not a worktree.
file write: path, operation, checkpoint id, diff stats); torn-tail repair on load.
flow (reusing the unified diff + computeDiffStats already produced for approval) with
"N files changed this step", and rollback to any checkpoint per file or per step.
Auto-approval paths get the same cards after the fact — the user can always see and
undo what the agent did.
Acceptance criteria
through the A4 atomic-publish helper; JSON state through safeWriteJson).
re-read-then-retry remediation, and the agent recovers automatically in the standard
loop.
the model recovers by reading first.
the user can view each step's diff and roll back per step or per file.
errors — deterministic, no corruption, no lost writes.
mcp_settings.json (MCP settings wiped when multiple windows open — race in McpHub.getMcpSettingsFilePath() direct fs.writeFile #1371).
(fix(task): guard saveClineMessages against abandoned tasks to prevent race in abandonSubtask #1021).
written to disk; the tool call errors instead (Truncated tool-call arguments can be silently written to disk (stale partial-parse nativeArgs reused on finalization failure) #1221).
delays on the default path).
Related PRs
Open (head: easonLiangWorldedtech's fork, base
main):Adjacent PRs to track (open on main):
Out of scope
visibility.
worktrees.