# How OpenCode Handles Concurrent File Modifications in the Multi-Edit Tool

> Learn how OpenCode's multi-edit tool manages concurrent file modifications using sequential Promise chains, read-time tracking, and staleness assertions to ensure data integrity.

- Repository: [Anomaly/opencode](https://github.com/anomalyco/opencode)
- Tags: internals
- Published: 2026-02-16

---

**The multi-edit tool in OpenCode prevents concurrent file modifications by executing edits sequentially through a per-file Promise chain lock, combined with read-time tracking and staleness assertions that abort operations if the file changes between read and write.**

The OpenCode repository provides a robust multi-edit tool designed to handle concurrent file modifications safely across multiple sessions. When several processes or sessions attempt to modify the same file simultaneously, the system prevents data corruption through a sophisticated three-layer protection mechanism. This article examines the implementation details in [`packages/opencode/src/tool/multiedit.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/tool/multiedit.ts) and its underlying concurrency controls.

## Multi-Edit Tool Architecture and Sequential Execution

The **multi-edit tool** acts as a thin wrapper around the single-file Edit tool. Rather than implementing independent file manipulation logic, it delegates to `EditTool.execute` while orchestrating multiple operations in strict sequence.

Located in [`packages/opencode/src/tool/multiedit.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/tool/multiedit.ts) (lines 26-38), the core execution loop processes the edits array sequentially:

```typescript
for (const [, edit] of params.edits.entries()) {
  const result = await tool.execute(
    {
      filePath: params.filePath,
      oldString: edit.oldString,
      newString: edit.newString,
      replaceAll: edit.replaceAll,
    },
    ctx,
  )
  results.push(result)
}

```

This sequential processing ensures that each edit applies to the state produced by the previous operation, preventing race conditions within the same multi-edit request.

## Three-Layer Concurrency Protection for File Modifications

The actual protection against **concurrent file modifications** from external sessions resides in the Edit tool and the FileTime utility. The system implements three distinct safety mechanisms in [`packages/opencode/src/file/time.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/file/time.ts):

### Read-Time Tracking with FileTime.read

Before any file modification, the system records the precise moment the file was last read. The `FileTime.read(sessionID, file)` function (lines 24-29) stores the current timestamp in a per-session map:

```typescript
// Conceptual implementation based on source
function read(sessionID: string, file: string): void {
  const timestamp = new Date();
  sessionMap.set(`${sessionID}:${file}`, timestamp);
}

```

This timestamp establishes the baseline for detecting external modifications that occur after the read but before the write operation.

### Exclusive Lock Acquisition with FileTime.withLock

To serialize concurrent write attempts, the system uses `FileTime.withLock(filepath, fn)` (lines 35-53). This function maintains a per-file Promise chain (`state.locks`) that ensures **only one edit runs at a time** for a given path:

```typescript
async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
  const currentLock = state.locks.get(filepath) || Promise.resolve();
  const newLock = currentLock.then(fn).finally(() => {
    if (state.locks.get(filepath) === newLock) {
      state.locks.delete(filepath);
    }
  });
  state.locks.set(filepath, newLock);
  return newLock;
}

```

Incoming edits automatically wait for the previous lock to resolve, creating a FIFO queue of operations for each file.

### Staleness Detection with FileTime.assert

Before committing changes, the system verifies the file remains unchanged since the last read. The `FileTime.assert(sessionID, filepath)` function (lines 55-68) compares the stored read timestamp against the file's current `mtime`:

```typescript
function assert(sessionID: string, filepath: string): void {
  const readTime = sessionMap.get(`${sessionID}:${filepath}`);
  const stats = fs.statSync(filepath);
  if (stats.mtime > readTime) {
    throw new Error(`File ${filepath} has been modified since it was last read...`);
  }
}

```

If the file's modification time is newer than the recorded read time, the operation aborts with an error, forcing the caller to re-read the file before proceeding.

## How Concurrent Modifications Are Serialized

When multiple sessions attempt **concurrent file modifications**, the three-layer protection works in concert to prevent conflicts:

1. **Session A** reads the file and records the timestamp via `FileTime.read`.
2. **Session B** acquires the file lock via `FileTime.withLock` and begins editing.
3. **Session B** completes the edit, updating the file's `mtime` and releasing the lock.
4. **Session A** attempts to edit, acquires the lock (waiting for B), then `FileTime.assert` detects the newer `mtime` and throws a staleness error.

This mechanism ensures that the second session must re-read the file to obtain the latest content before attempting modifications, effectively implementing an optimistic concurrency control pattern.

## Practical Implementation Examples

### Basic Multi-Edit Request Structure

The multi-edit tool accepts a JSON payload specifying the file path and an array of edit operations. Each edit contains the old string to match and the new string to substitute:

```json
{
  "filePath": "/home/user/project/src/config.ts",
  "edits": [
    {
      "oldString": "debug: false",
      "newString": "debug: true"
    },
    {
      "oldString": "timeout = 30",
      "newString": "timeout = 60",
      "replaceAll": true
    }
  ]
}

```

The tool processes these edits sequentially, applying each transformation to the result of the previous operation while maintaining the concurrency protections described above.

### Handling Stale Reads Across Sessions

When multiple processes interact with the same file, the staleness detection prevents conflicting updates:

```typescript
// Session A reads the file
await ReadTool.execute({ filePath: "/tmp/example.txt" }, ctxA);

// Session B performs a multi-edit on the same file
await MultiEditTool.execute({
  filePath: "/tmp/example.txt",
  edits: [{ oldString: "foo", newString: "bar" }],
}, ctxB);

```

If Session B modifies the file before Session A attempts its own edit, `FileTime.assert` in the Edit step will throw:

```

Error: File /tmp/example.txt has been modified since it was last read...

```

Session A must then invoke the Read tool again to refresh its cached timestamp before proceeding with modifications.

## Summary

- The **multi-edit tool** in OpenCode ([`packages/opencode/src/tool/multiedit.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/tool/multiedit.ts)) processes multiple edits sequentially by delegating to the single-file Edit tool in a loop.
- **Concurrent file modifications** are prevented through a three-layer protection system in [`packages/opencode/src/file/time.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/file/time.ts): read-time tracking, per-file Promise chain locks, and staleness assertions.
- The **FileTime.withLock** function ensures only one edit operation executes at a time per file path, automatically serializing concurrent requests into a FIFO queue.
- **Staleness detection** via `FileTime.assert` aborts operations if the file was modified externally since the last read, forcing clients to refresh their view before editing.
- This architecture guarantees data integrity when multiple sessions attempt simultaneous modifications to the same file.

## Frequently Asked Questions

### What happens if two users edit the same file simultaneously?

When two sessions attempt concurrent file modifications, the second session waits for the first to release the per-file lock acquired via `FileTime.withLock`. After the first session completes and updates the file's `mtime`, the second session acquires the lock, but `FileTime.assert` detects the newer modification time and throws a staleness error. The second user must re-read the file to obtain the latest content before attempting their edit.

### How does the multi-edit tool differ from the single-edit tool?

The **multi-edit tool** ([`packages/opencode/src/tool/multiedit.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/tool/multiedit.ts)) functions as a sequential orchestrator that delegates individual operations to the **single-edit tool** ([`packages/opencode/src/tool/edit.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/tool/edit.ts)). While the single-edit tool performs one string replacement and implements the three-layer concurrency protection, the multi-edit tool simply loops through an array of edits, applying each transformation to the result of the previous operation without implementing additional locking logic itself.

### What error message appears when a file is modified during editing?

When `FileTime.assert` detects that a file's `mtime` is newer than the recorded read timestamp stored in the per-session map, it throws an error with the message format: `File ${filepath} has been modified since it was last read...`. This error forces the calling session to invoke the Read tool again to refresh its cached timestamp before proceeding with any edit operations.

### Is the multi-edit tool atomic?

No, the multi-edit tool is **not atomic**. While individual edits within the sequence are protected by `FileTime.withLock`, the entire batch of edits is not wrapped in a single atomic transaction. If the fifth edit in a sequence of ten fails the staleness check because another session modified the file, the first four edits will have already been applied to the file. Users must handle partial failure scenarios by re-reading the file and retrying the complete edit sequence if necessary.