fix(server): convert sync fs I/O to async across server API and session memory#2219
Merged
Conversation
5 tasks
… session memory Replace blocking readFileSync/writeFileSync/readdirSync/statSync/existsSync with non-blocking readFile/writeFile/readdir/stat from node:fs/promises in all server API handlers and session.ts. This prevents the event loop from blocking on disk I/O during concurrent HTTP requests. - assets.ts: async cached reads (resolveAssetDir stays sync for module load) - skills.ts, memory.ts, hooks.ts, browse.ts, project-tree.ts, files.ts, checkpoint-diffs.ts, health.ts, sessions.ts: async handlers - session.ts: added async variants (listSessionsAsync, deleteSessionAsync, loadSessionMessagesAsync, readTailMessagesAsync, etc.) - atomic-write.ts: added async atomicWrite alongside sync atomicWriteSync - index.ts: await renderIndexHtml/serveAsset in dispatch Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- files.ts: add explicit Stats type annotation (noImplicitAnyLet) - browse.ts: organize imports - assets.ts, skills.ts, memory.ts, sessions.ts, session.ts, atomic-write.ts: biome format (line length, trailing commas) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tests These functions now return Promises after the sync→async conversion, so the test calls must await them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1ce353b to
7da4fc1
Compare
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Read the file directly instead of stat-then-read to avoid a window where the file could be deleted between the two calls. CodeQL flagged this as a potential file system race condition. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
esengine
approved these changes
May 29, 2026
esengine
left a comment
Owner
There was a problem hiding this comment.
Reviewed — sound, additive async conversion of the dashboard server's I/O. Key checks pass:
- Additive:
atomicWriteSync(and the other sync helpers) are kept for CLI/agent-loop callers; the newatomicWrite/loadSessionMetaAsync/patchSessionMetaAsync/readSessionMessagesAsync/countLinesAsyncare used only by the server API handlers — so the loop's ordering guarantees aren't touched. - Properly awaited: every new async call is
awaited (no fire-and-forget / missing-await races). - atomicWrite parity: the async version mirrors
atomicWriteSync— writeFile(tmp) → chmod → rename with the EXDEV copyFile+unlink fallback and tmp cleanup on error. - CI (incl. dashboard-smoke) is green now.
Good responsiveness win — sync fs in a request handler blocks the whole event loop, so moving the server handlers off it is the right call. Merging.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
readFileSync/writeFileSync/readdirSync/statSync/existsSyncwith non-blockingreadFile/writeFile/readdir/statfromnode:fs/promisesin all server API handlers and session memory modulesession.tsfunctions (listSessionsAsync,deleteSessionAsync,loadSessionMessagesAsync,readTailMessagesAsync,appendSessionMessageAsync,rewriteSessionAsync,renameSessionAsync,archiveSessionAsync,listSessionsForWorkspaceAsync,pruneStaleSessionsAsync,patchSessionMetaAsync,loadSessionMetaAsync,findSessionsByPrefixAsync)atomicWritealongside existing syncatomicWriteSyncinatomic-write.tshealth.ts,sessions.ts) updated to use async session functionsFiles changed (13)
src/core/atomic-write.tsatomicWrite()functionsrc/memory/session.tssrc/server/api/browse.tslistWindowsDrives,readSubdirs, handlersrc/server/api/checkpoint-diffs.tsreadFileSync→await readFilesrc/server/api/files.tswalk()withreaddir/statsrc/server/api/health.tsdirSize(), parallelPromise.all,listSessionsAsyncsrc/server/api/hooks.tsreadSettingsFile/writeSettingsFilesrc/server/api/memory.tslistMemoryFiles, all handler opssrc/server/api/project-tree.tsbuildTree/walkwithreaddir/statsrc/server/api/sessions.tsparseTranscript,listSessionsAsync,deleteSessionAsyncsrc/server/api/skills.tsreadSkillListEntry,resolveSkillPath,listSkillssrc/server/assets.tsresolveAssetDirstays sync (module load)src/server/index.tsawait renderIndexHtml/serveAssetindispatch()Motivation
The server API handlers were already
asyncbut called sync fs functions internally, blocking the event loop on every request. With concurrent dashboard requests (SSE stream + API calls + asset fetches), this creates unnecessary serialization. Converting to async I/O allows the server to serve other requests while waiting on disk.The sync variants in
session.tsare preserved — the TUI and prompt-building layers still use them and their callers aren't async. The new*Asyncvariants are opt-in for server-side callers.Test plan
npm run buildpasses🤖 Generated with Claude Code