# What Is the File Mutation Queue in Prime‑Agent and How Does It Handle Concurrent Edits?

> Discover Prime-Agent's file mutation queue. Learn how it serializes edits to the same file and allows parallel execution for different files, preventing concurrent edit conflicts.

- Repository: [Prime Intellect/prime-agent](https://github.com/PrimeIntellect-ai/prime-agent)
- Tags: internals
- Published: 2026-08-12

---

**Prime‑Agent's file mutation queue is a lightweight coordination primitive that guarantees serial execution of all mutations targeting the same filesystem path while allowing mutations to different files to run in parallel.**

The file mutation queue is essential infrastructure in the Prime‑Agent coding agent. It prevents race conditions when multiple tools or async operations attempt to read and write the same file simultaneously. This article explains the queue's implementation in `packages/coding-agent/src/core/tools/file‑mutation‑queue.ts` and how it achieves both safety and performance.

## How the File Mutation Queue Works

The queue uses a promise‑chain pattern rather than heavy locks or mutexes. This design keeps overhead minimal while ensuring strict ordering per file.

### Canonicalizing the Target Path

Before any queue operation, the path is normalized via `getMutationQueueKey()`. This function:

1. Resolves the user‑supplied path to an absolute path
2. Calls `realpathSync.native` to dereference symlinks

The resulting canonical string becomes the queue key. This ensures that [`/tmp/file.txt`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main//tmp/file.txt) and [`/tmp/link.txt`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main//tmp/link.txt) (pointing to the same inode) share one logical queue, preventing split‑brain scenarios.

```typescript
// From packages/coding-agent/src/core/tools/file-mutation-queue.ts
// Lines 6-12 (conceptual)
function getMutationQueueKey(rawPath: string): string {
  const absolute = path.resolve(rawPath);
  try {
    return fs.realpathSync.native(absolute);
  } catch {
    return absolute; // Fallback if path doesn't exist yet
  }
}

```

### Per‑Key Promise Chain Storage

A `Map<string, Promise<void>>` named `fileMutationQueues` stores the tail promise of each file's active chain. When no operations are pending for a file, no entry exists in the map.

```typescript
// Lines 4-5 (conceptual)
const fileMutationQueues = new Map<string, Promise<void>>();

```

### Appending New Slots to the Chain

When `withFileMutationQueue()` is invoked, the current tail is retrieved (or a resolved promise if the file has no pending operations). A new deferred promise is created and chained:

```typescript
// Lines 23-28 (conceptual)
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
const nextQueue = new Promise<void>((resolve) => { releaseNext = resolve; });
const chainedQueue = currentQueue.then(() => nextQueue);
fileMutationQueues.set(key, chainedQueue);

```

The `chainedQueue` becomes the new tail. Critically, `nextQueue` does not resolve until `releaseNext` is called—this happens **after** the user's mutation function completes.

### Executing the Mutation

The mutation runs only after all prior operations on that file have finished:

```typescript
// Lines 30-33 (conceptual)
await currentQueue;         // Wait for previous mutations
try {
  return await fn();        // Run the supplied mutation function
} finally {
  releaseNext();            // Unblock the next queued operation
}

```

### Cleanup and Memory Management

After the mutation completes (success or failure), the queue cleans up:

```typescript
// Lines 34-38 (conceptual)
// If this slot is still the tail, remove it to prevent memory leaks
if (fileMutationQueues.get(key) === chainedQueue) {
  fileMutationQueues.delete(key);
}

```

This lazy cleanup ensures the map stays bounded even under high churn.

## File Mutation Queue vs. Unprotected Writes

| Aspect | Unprotected `fs.writeFile` | With File Mutation Queue |
|--------|---------------------------|--------------------------|
| **Same‑file concurrency** | Race conditions, data loss | Strict serialization |
| **Cross‑file concurrency** | Parallel (but unsafe) | Parallel and safe |
| **Memory overhead** | None | One map entry per active file |
| **Ordering guarantee** | None | FIFO per file |

## Practical Code Examples

### Serializing Two Edits to the Same File

```typescript
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import * as fs from "fs";

await Promise.all([
  withFileMutationQueue("/tmp/shared.txt", async () => {
    await fs.promises.appendFile("/tmp/shared.txt", "first\n");
  }),
  withFileMutationQueue("/tmp/shared.txt", async () => {
    await fs.promises.appendFile("/tmp/shared.txt", "second\n");
  })
]);
// Result: "first\nsecond\n" or "second\nfirst\n" depending on call order
// Never: truncated or interleaved content

```

Despite `Promise.all`, the second `withFileMutationQueue` call blocks until the first releases its slot. The file mutation queue enforces this ordering transparently.

### Parallel Edits to Different Files

```typescript
await Promise.all([
  withFileMutationQueue("/tmp/a.txt", async () => {
    await fs.promises.writeFile("/tmp/a.txt", "A");
  }),
  withFileMutationQueue("/tmp/b.txt", async () => {
    await fs.promises.writeFile("/tmp/b.txt", "B");
  })
]);
// Both writes execute simultaneously—different queue keys, no blocking

```

### Using the Queue in Custom Tools

Custom tools that mutate files should wrap their filesystem operations:

```typescript
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import * as path from "path";
import * as fs from "fs";

async function myToolEdit(ctx: ToolContext, targetPath: string, newContent: string) {
  const absolutePath = path.resolve(ctx.cwd, targetPath);
  
  return withFileMutationQueue(absolutePath, async () => {
    await fs.promises.writeFile(absolutePath, newContent);
    return { ok: true, bytesWritten: newContent.length };
  });
}

```

As documented in [`packages/coding-agent/docs/extensions.md`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/docs/extensions.md), this pattern is required for any custom tool performing file mutations to maintain Prime‑Agent's consistency guarantees.

## Key Source Files

- **`packages/coding-agent/src/core/tools/file‑mutation‑queue.ts`** — Core implementation of the per‑file promise queue
- **[`packages/coding-agent/src/core/tools/edit.ts`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/core/tools/edit.ts)** — Built‑in `edit` tool uses the queue for all file writes
- **[`packages/coding-agent/docs/extensions.md`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/docs/extensions.md)** — Documentation for custom tool authors
- **`packages/coding-agent/test/file‑mutation‑queue.test.ts`** — Test coverage for serialization and parallelism

## Summary

- The **file mutation queue** in Prime‑Agent serializes all operations targeting the same filesystem path using a lightweight promise‑chain pattern
- **Canonical path resolution** via `realpathSync.native` ensures symlinks don't create duplicate queues
- **Per‑file parallel execution** is preserved—operations on different files never block each other
- **Automatic cleanup** removes idle queue entries to prevent memory leaks
- The built‑in `edit` tool and documented custom tool patterns rely on this queue for deterministic, race‑free file mutations

## Frequently Asked Questions

### What happens if two tools try to edit the same file simultaneously?

Prime‑Agent's file mutation queue orders them sequentially. The second tool's `withFileMutationQueue` call chains behind the first and starts only after the first mutation completes. This prevents read‑modify‑write races where both tools might base their edits on stale file contents.

### Does the file mutation queue block all file operations?

No. The queue blocks only operations targeting the **same canonical path**. Mutations to different files proceed in parallel with no overhead. This design maximizes throughput while guaranteeing safety.

### How does the queue handle files that don't exist yet?

`getMutationQueueKey()` falls back to the absolute path if `realpathSync.native` throws (typically because the file doesn't exist). Once the file is created, subsequent calls canonicalize to the same key. The queue remains correct because all operations for that logical file share one chain regardless of when the file appears.

### Is the file mutation queue required for custom tools?

According to [`packages/coding-agent/docs/extensions.md`](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/docs/extensions.md), any custom tool that performs file mutations **must** use `withFileMutationQueue`. Omitting it risks data corruption when the tool runs concurrently with Prime‑Agent's built‑in file operations or other custom tools.