Skip to content

[EPIC] File Write Safety Prevent Concurrent Write Races Data Corruption #1375

Description

@easonLiangWorldedtech

Problem

The agent's file-write path has no version guard, no atomic publish, and no per-step
visibility:

  • Concurrent writers silently overwrite each other's work: parallel subtasks share one
    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).
  • Model writes land directly in the shared workspace with no checkpoint to roll back to;
    task start creates no baseline at all.
  • No staleness detection: a file that changed after the model read it is silently
    overwritten on the next write.
  • The write path carries artificial latency (1 s delay per write) on top of the missing
    safety.

Known issues

# Title Status Fix in this epic
#1371 MCP settings wiped by concurrent windows Open A5 — root cause is the check-then-create TOCTOU in McpHub.getMcpSettingsFilePath() (L496-517), not initializeMcpServers()
#1021 Race in abandonSubtask for saveClineMessages Open A5 — fire-and-forget saveClineMessages() re-attaches abandoned subtasks
#920 Concurrent task history updates cause lost entries Open A5 — cross-instance (parallel tabs) regression test on the locked-merge write
#1231 Global _index.json full rewrite unsafe (corruption confirmed) Closed Already fixed — per-task history_item.json + merge-under-lock; that is now the reference storage pattern
#1221 Truncated tool-call args silently written to disk Open A6 (separate story) — parser/finalization defect, not a filesystem-safety problem
#376 File-level write serialization Open Superseded by A3
#370 Extension-side task-scoping guard Open Related; stays open

Current state (verified in code)

Agent write path

  • 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

Concurrency

  • 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.
  • A5 Internal-state firebreak
  • A6 Truncated tool-call args (Truncated tool-call arguments can be silently written to disk (stale partial-parse nativeArgs reused on finalization failure) #1221, separate story) — a NativeToolCallParser
    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:

Current slow point Location Action
1000 ms delay per write DEFAULT_WRITE_DELAY_MS (packages/types/src/global-settings.ts:23) 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

Related PRs

Open (head: easonLiangWorldedtech's fork, base main):

PR Scope
#1380 A5 — #1371: atomic mcp_settings.json stub creation via safeWriteJson + merge under the advisory lock
#1381 Speed: DEFAULT_WRITE_DELAY_MS 1000 → 0, and the delay(300) before scrollToFirstDiff removed
#1382 A5 — #1021: guard saveClineMessages against abandoned tasks (minimal main form of commit 1d1eb91)

Adjacent PRs to track (open on main):

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions