Skip to content

fix(server): convert sync fs I/O to async across server API and session memory#2219

Merged
esengine merged 5 commits into
esengine:mainfrom
HUQIANTAO:fix/memory-async-io
May 29, 2026
Merged

fix(server): convert sync fs I/O to async across server API and session memory#2219
esengine merged 5 commits into
esengine:mainfrom
HUQIANTAO:fix/memory-async-io

Conversation

@HUQIANTAO

Copy link
Copy Markdown
Contributor

Summary

  • 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 memory module
  • Add async variants of key session.ts functions (listSessionsAsync, deleteSessionAsync, loadSessionMessagesAsync, readTailMessagesAsync, appendSessionMessageAsync, rewriteSessionAsync, renameSessionAsync, archiveSessionAsync, listSessionsForWorkspaceAsync, pruneStaleSessionsAsync, patchSessionMetaAsync, loadSessionMetaAsync, findSessionsByPrefixAsync)
  • Add async atomicWrite alongside existing sync atomicWriteSync in atomic-write.ts
  • Server API callers (health.ts, sessions.ts) updated to use async session functions

Files changed (13)

File Change
src/core/atomic-write.ts Added async atomicWrite() function
src/memory/session.ts Added 14 async function variants + async fs imports
src/server/api/browse.ts Async listWindowsDrives, readSubdirs, handler
src/server/api/checkpoint-diffs.ts readFileSyncawait readFile
src/server/api/files.ts Async walk() with readdir/stat
src/server/api/health.ts Async dirSize(), parallel Promise.all, listSessionsAsync
src/server/api/hooks.ts Async readSettingsFile/writeSettingsFile
src/server/api/memory.ts Async listMemoryFiles, all handler ops
src/server/api/project-tree.ts Async buildTree/walk with readdir/stat
src/server/api/sessions.ts Async parseTranscript, listSessionsAsync, deleteSessionAsync
src/server/api/skills.ts Async readSkillListEntry, resolveSkillPath, listSkills
src/server/assets.ts Async cached reads; resolveAssetDir stays sync (module load)
src/server/index.ts await renderIndexHtml/serveAsset in dispatch()

Motivation

The server API handlers were already async but 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.ts are preserved — the TUI and prompt-building layers still use them and their callers aren't async. The new *Async variants are opt-in for server-side callers.

Test plan

  • npm run build passes
  • Dashboard loads and renders correctly
  • Session list loads in dashboard sidebar
  • Session delete works from dashboard
  • Skill editor loads and saves
  • Memory editor loads and saves
  • Health endpoint returns valid JSON
  • File picker (@-mention) returns results

🤖 Generated with Claude Code

Comment thread src/server/api/memory.ts Fixed
HUQIANTAO and others added 3 commits May 29, 2026 13:04
… 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>
@HUQIANTAO HUQIANTAO force-pushed the fix/memory-async-io branch from 1ce353b to 7da4fc1 Compare May 29, 2026 05:04
HUQIANTAO and others added 2 commits May 29, 2026 13:09
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 esengine left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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 new atomicWrite / loadSessionMetaAsync / patchSessionMetaAsync / readSessionMessagesAsync / countLinesAsync are 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.

@esengine esengine merged commit 249e18f into esengine:main May 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants