How OpenCode Handles Concurrent File Modifications in the Multi-Edit Tool
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 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 (lines 26-38), the core execution loop processes the edits array sequentially:
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:
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:
// 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:
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:
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:
- Session A reads the file and records the timestamp via
FileTime.read. - Session B acquires the file lock via
FileTime.withLockand begins editing. - Session B completes the edit, updating the file's
mtimeand releasing the lock. - Session A attempts to edit, acquires the lock (waiting for B), then
FileTime.assertdetects the newermtimeand 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:
{
"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:
// 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) 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: 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.assertaborts 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) functions as a sequential orchestrator that delegates individual operations to the single-edit tool (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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →