You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Phase 1 shipped in #4096. Post-release the rename-based atomic write was found to reset file ownership in Docker and shared-workspace setups (POSIX rename creates a new inode owned by the writing process's euid:egid, silently dropping the original uid:gid). Mitigation PR #4431 adds an ownership-preserving fallback inside atomicWriteFile; whether to also revert user-file write paths (Write / Edit / NotebookEdit) to plain in-place fs.writeFile is still under evaluation. Full trade-off analysis: docs/design/2026-05-22-atomic-file-write-strategy.md.
Context
The core file write paths in Qwen Code (Write tool, Edit tool) use bare fs.writeFile. If the process crashes or power is lost mid-write, the file is left half-written and corrupt. The codebase already acknowledges this — write-file.ts:371-385 and edit.ts:487-497 both contain explicit TODOs:
the only way to close it is an atomic write (write-to-temp + rename)... deferred to a follow-up
Qwen Code already has atomicWriteJSON (6 call sites across 2 files) but it only supports JSON and lacks flush: true (fsync). This issue tracks the full rollout of atomic writes and data safety mechanisms.
Symlink resolution: before writing, call fs.realpath() to resolve symlinks. Write the tmp file next to the real target (not next to the symlink) and rename to the real target path. This prevents rename('tmp', 'symlink') from replacing the symlink itself instead of writing through it. (Matches Claude Code's readlinkSync + resolve pattern in writeFileSyncAndFlush_DEPRECATED)
Write temp file in the same directory as the resolved target to guarantee rename stays on the same filesystem
Fallback to direct write on EXDEV; cleanup tmp and rethrow on other errors
Export renameWithRetry for reuse by other modules
Refactor atomicWriteJSON to delegate to atomicWriteFile internally (adding missing flush: true)
1.2 Deduplicate renameWithRetry
File: packages/core/src/utils/runtimeStatus.ts
Remove the private renameWithRetry at runtimeStatus.ts:220-239 (identical to atomicFileWrite.ts:50-72)
Replace with import { renameWithRetry } from './atomicFileWrite.js'
Refactor writeRuntimeStatus() inline tmp+rename (L110-121) to use atomicWriteJSON
1.3 Wire fileSystemService.writeTextFile() to atomic write
Note: The second FileSystemService implementation (AcpFileSystemService in cli/src/acp-integration/service/filesystem.ts) either delegates to a remote ACP connection or falls back to a StandardFileSystemService instance — no changes needed there.
The flush option in fs.writeFile/fs.writeFileSync was added in Node 21.2. The project requires Node >=22 (compatible), but @types/node is pinned to ^20.11.24 in packages/cli/package.json (L90), which may lack the flush type definition. Upgrade to @types/node >= 22 to avoid needing type assertions.
1.6 Tests
File: packages/core/src/utils/atomicFileWrite.test.ts (already exists with 5 tests for atomicWriteJSON)
Append new test cases for atomicWriteFile:
Writes string content to a new file
Writes Buffer content to a new file
Preserves existing file permissions
Sets explicit mode via options
Does not leave temp files on success
Cleans up temp file on write failure
Cleans up temp file on rename failure
Falls back to direct write on EXDEV error
Resolves symlinks correctly (writes through symlink to real target)
Note
Phase 1 shipped in #4096. Post-release the rename-based atomic write was found to reset file ownership in Docker and shared-workspace setups (POSIX
renamecreates a new inode owned by the writing process'seuid:egid, silently dropping the originaluid:gid). Mitigation PR #4431 adds an ownership-preserving fallback insideatomicWriteFile; whether to also revert user-file write paths (Write / Edit / NotebookEdit) to plain in-placefs.writeFileis still under evaluation. Full trade-off analysis:docs/design/2026-05-22-atomic-file-write-strategy.md.Context
The core file write paths in Qwen Code (Write tool, Edit tool) use bare
fs.writeFile. If the process crashes or power is lost mid-write, the file is left half-written and corrupt. The codebase already acknowledges this —write-file.ts:371-385andedit.ts:487-497both contain explicit TODOs:Qwen Code already has
atomicWriteJSON(6 call sites across 2 files) but it only supports JSON and lacksflush: true(fsync). This issue tracks the full rollout of atomic writes and data safety mechanisms.Related issues
writeLine/writeLineSynclacking fsync). Phase 2 Tier 3 (logger fixes) in this issue subsumes that scope; completing it should close chore(core): jsonl reader/writer follow-ups from #3656 #3681.}{glued records on session JSONL load (#3606) #3656 (merged) — read-side recovery for}{glued records in session JSONL. Explicitly left write-side fixes out of scope.qwen --resumewithout an ID to choose from existing sessions. #3606 — original corruption report that motivated fix(core): recover from}{glued records on session JSONL load (#3606) #3656.Reference:
Phase 1: Generic atomic write + core path integration (~120 lines / 0.5 day)
Goal: Make all Write and Edit tool file operations atomic, eliminating crash-induced file corruption.
1.1 Extend
atomicFileWrite.ts— addatomicWriteFile()File:
packages/core/src/utils/atomicFileWrite.tsatomicWriteFile(filePath, data: string | Buffer, options?)functionflush: true(fsync), permission preservation (stat target mode → chmod tmp), encodingfs.realpath()to resolve symlinks. Write the tmp file next to the real target (not next to the symlink) and rename to the real target path. This preventsrename('tmp', 'symlink')from replacing the symlink itself instead of writing through it. (Matches Claude Code'sreadlinkSync+ resolve pattern inwriteFileSyncAndFlush_DEPRECATED)renameWithRetryfor reuse by other modulesatomicWriteJSONto delegate toatomicWriteFileinternally (adding missingflush: true)1.2 Deduplicate
renameWithRetryFile:
packages/core/src/utils/runtimeStatus.tsrenameWithRetryatruntimeStatus.ts:220-239(identical toatomicFileWrite.ts:50-72)import { renameWithRetry } from './atomicFileWrite.js'writeRuntimeStatus()inline tmp+rename (L110-121) to useatomicWriteJSON1.3 Wire
fileSystemService.writeTextFile()to atomic writeFile:
packages/core/src/services/fileSystemService.tsReplace all 4 bare
fs.writeFilecalls inStandardFileSystemService.writeTextFile()(L214-262):fs.writeFile(filePath, Buffer.concat(...))atomicWriteFile(filePath, Buffer.concat(...))fs.writeFile(filePath, encoded)atomicWriteFile(filePath, encoded)fs.writeFile(filePath, Buffer.concat(...))atomicWriteFile(filePath, Buffer.concat(...))fs.writeFile(filePath, content, 'utf-8')atomicWriteFile(filePath, content, { encoding: 'utf-8' })1.4 Add fsync to
writeWithBackup.tsFile:
packages/cli/src/utils/writeWithBackup.tsLine 81 uses
fs.writeFileSync(tempPath, content, { encoding })(synchronous). Addflush: true:1.5 Upgrade
@types/nodeThe
flushoption infs.writeFile/fs.writeFileSyncwas added in Node 21.2. The project requires Node >=22 (compatible), but@types/nodeis pinned to^20.11.24inpackages/cli/package.json(L90), which may lack theflushtype definition. Upgrade to@types/node>= 22 to avoid needing type assertions.1.6 Tests
File:
packages/core/src/utils/atomicFileWrite.test.ts(already exists with 5 tests foratomicWriteJSON)Append new test cases for
atomicWriteFile:Phase 2: Batch fix remaining bare
fs.writeFilecalls (~80 lines / 0.5 day)Goal: Replace all other high-risk bare
fs.writeFilecalls with atomic writes.Tier 1 — Security-sensitive (credentials/tokens)
core/src/mcp/oauth-token-storage.ts(L102, L182)atomicWriteFile(path, data, { mode: 0o600 })core/src/mcp/token-storage/file-token-storage.ts(L103)atomicWriteFile(path, encrypted, { mode: 0o600 })core/src/qwen/qwenOAuth2.ts(L982)atomicWriteFile(path, credString, { mode: 0o600 })core/src/qwen/sharedTokenManager.ts(L639)flush: true+ use sharedrenameWithRetryTier 2 — Data integrity (Memory subsystem)
core/src/memory/manager.ts(L291)atomicWriteJSONcore/src/memory/extract.ts(L93, L118)atomicWriteFilecore/src/memory/indexer.ts(L81)atomicWriteJSONcore/src/memory/dream.ts(L125)atomicWriteFilecore/src/memory/forget.ts(L225, L290)atomicWriteFileTier 3 — Configuration & session durability (subsumes #3681)
cli/src/config/trustedFolders.ts(L182)atomicWriteFilecore/src/core/logger.ts(L160, L231, L338)atomicWriteFile/atomicWriteJSON; addflush: truetowriteLine/writeLineSyncappend paths (closes #3681)Phase 3: FileCheckpointService (~400 lines / 2 days)
Goal: Per-turn automatic file snapshots with
/rewindsupport for precise rollback to any message point.Based on Claude Code's
fileHistory.ts:packages/core/src/services/fileCheckpointService.tstrackBeforeEdit(filePath, messageId)— called at the start of Write/Edit toolexecute()to back up the file before modificationmakeSnapshot(messageId)— called at the end of each agent turnrestoreToSnapshot(messageId)— restore all files to a given message point~/.qwen-code/sessions/<sessionId>/checkpoints/<contentHash>@v<N>/rewindcommandPhase 4: Tool result disk overflow (~150 lines / 1 day)
Goal: Spill oversized tool outputs to disk to prevent OOM and context pollution.
Based on Claude Code's
toolResultStorage.ts:packages/core/src/services/toolResultPersistence.tsOVERFLOW_THRESHOLD = 50KB(~12K tokens)~/.qwen-code/sessions/<sessionId>/tool-results/<toolUseId>.txt/clearor session exitcontentGenerator.tswhen collecting tool resultsEstimated effort
atomicWriteFile+ core pathsTotal: ~750 lines / 4 days
Recommended execution order: Phase 1 → 2 → 3 → 4. Phase 1 is the minimum viable change that directly closes the TODOs already marked in the code.
🤖 Generated with Qwen Code