Skip to content

fix(coding-agent): use async operations in tools#4756

Closed
mitsuhiko wants to merge 10 commits into
mainfrom
async-file-tools
Closed

fix(coding-agent): use async operations in tools#4756
mitsuhiko wants to merge 10 commits into
mainfrom
async-file-tools

Conversation

@mitsuhiko

@mitsuhiko mitsuhiko commented May 19, 2026

Copy link
Copy Markdown
Member

On Windows the Microsoft Defender can make things hang and when it hangs, the TUI also locks up. This moves some of the more likely sync fs operations that can happen while during streaming to async operations.

Additionally this moves the image handling (resizing) into a worker.

@mitsuhiko mitsuhiko requested a review from badlogic May 19, 2026 14:05
@badlogic badlogic added the inprogress Issue is being worked on label May 19, 2026
import { resolve } from "node:path";

const fileMutationQueues = new Map<string, Promise<void>>();
let registrationQueue = Promise.resolve();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the registrationQueue was the simplest thing i could come up with that retains the "execute everything in order" behavior from before.

@badlogic

Copy link
Copy Markdown
Collaborator

i see no better way to do the registration ordering if we have to make realpath async. however, the tools themselves have races. i let the clanker explain:

There are two separate race questions here.

  1. The async key/registration race

If getMutationQueueKey() becomes async, this naive version is broken:

const key = await getMutationQueueKey(filePath);
const currentQueue = fileMutationQueues.get(key);
fileMutationQueues.set(key, chainedQueue);

Two same-file operations can both resolve keys concurrently, and whichever realpath() finishes first registers first. The registrationQueue prevents that by making registration FIFO, so for queue registration ordering, this mostly works. It is not pretty because it serializes registration globally, but some ordered admission step is required if realpath is async and we still want arrival order preserved.

  1. The existing tool abort race

The bigger problem is in the tools using the queue. They have this shape:

return withFileMutationQueue(path, () =>
	new Promise((resolve, reject) => {
		const onAbort = () => {
			aborted = true;
			reject(new Error("Operation aborted"));
		};

		(async () => {
			await ops.writeFile(...);
			if (aborted) return;
			resolve(...);
		})();
	}),
);

That means:

  1. queued write/edit A starts
  2. A is inside await ops.writeFile(...) or another async filesystem operation
  3. abort fires
  4. abort handler immediately rejects the outer promise
  5. withFileMutationQueue thinks A is done and releases the queue
  6. queued write/edit B starts
  7. A's underlying filesystem operation may still complete later

So the queue serializes the lifetime of the wrapper promise, not necessarily the lifetime of the actual filesystem operation. Abort can release the queue early.

The queued mutation body should not use a detached async IIFE plus immediate abort rejection. It should be an async function, observe abort between awaited operations, and only return/reject after the currently awaited filesystem operation has actually settled. Rough shape:

return withFileMutationQueue(absolutePath, async () => {
	if (signal?.aborted) throw new Error("Operation aborted");

	let aborted = false;
	const onAbort = () => {
		aborted = true;
	};

	signal?.addEventListener("abort", onAbort, { once: true });
	try {
		await ops.access(absolutePath);
		if (aborted) throw new Error("Operation aborted");

		const buffer = await ops.readFile(absolutePath);
		if (aborted) throw new Error("Operation aborted");

		await ops.writeFile(absolutePath, content);
		if (aborted) throw new Error("Operation aborted");

		return result;
	} finally {
		signal?.removeEventListener("abort", onAbort);
	}
});

So I don't think the registration ordering is the main issue. The queued tool bodies need to be fixed too, otherwise the queue can still be bypassed by abort timing.

@mitsuhiko mitsuhiko changed the title fix(coding-agent): use async filesystem operations in tools fix(coding-agent): use async operations in tools May 20, 2026
@badlogic

Copy link
Copy Markdown
Collaborator

Wrapped this locally with the follow-up cleanup:

  • kept async filesystem changes and queue admission ordering
  • removed abort listeners from queued edit/write bodies so aborts do not release the mutation queue while a filesystem operation is still in flight
  • reused the async path-existence helper from ls/find
  • fixed Bun binary image resizing to try the embedded worker entrypoint before falling back in-process
  • built a local Bun release for manual verification

This comment is AI-generated by /wr

@badlogic badlogic closed this May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inprogress Issue is being worked on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants