Skip to content

[codex] fix trash and runtime sync churn#27

Draft
ComBba wants to merge 2 commits into
mainfrom
codex/fix-trash-retention-cap
Draft

[codex] fix trash and runtime sync churn#27
ComBba wants to merge 2 commits into
mainfrom
codex/fix-trash-retention-cap

Conversation

@ComBba
Copy link
Copy Markdown
Contributor

@ComBba ComBba commented Jun 8, 2026

Summary

Fixes #26.

This adds the missing guardrails around rsync trash growth and prevents high-churn Claude Desktop and Codex runtime artifacts from entering sync in the first place.

Root Cause

Code inspection showed four compounding issues:

  • RsyncCommandBuilder creates a fresh ~/.claudesync/trash/<job-id>/ bucket for every rsync job that uses --backup-dir.
  • TrashJanitor only removed UUID buckets older than the retention window, with no size cap, bucket-count cap, or empty-bucket cleanup.
  • FileSyncActor explodes top-level full syncs into immediate subdirectory jobs before rsync filtering, so existing target excludes such as Local Storage/ did not prevent those subdirs from being queued. The logs confirmed the same path for vm_bundles/, Code Cache/, and DawnWebGPUCache/.
  • codexConfig was treated as a realtime target but only excluded *.log, cache/, and *.tmp, so active Codex sessions, SQLite/WAL state, package caches, helper runtimes, and temp directories could repeatedly trigger realtime syncs.

Runtime/log evidence from this machine before the fix:

  • ~/.claudesync/logs/claudesync.log* contained 10,422 --backup-dir= rsync invocations.
  • The same logs contained 157 vm_bundles rsync entries.
  • Live Claude Desktop vm_bundles was 37G, with large VM image files including rootfs.img, rootfs.img.zst, sessiondata.img, and hidden .rootfs.img.* variants.
  • Live ~/.codex was 1.3G despite the PRD expectation that the Codex config target stays under 10MB; most of that was runtime churn (sessions 471M, packages 389M, .tmp 187M, logs_2.sqlite 136M, plus WAL/SQLite/helper directories).
  • ~/.claudesync/preferences.json had no trashRetentionDays, so the app used the old compiled default.

Changes

  • Exclude Claude Desktop local-agent VM/cache artifacts by default: vm_bundles/, *.img, *.raw, *.zst, Code Cache/, and DawnWebGPUCache/.
  • Exclude Codex runtime/session churn from codexConfig: sessions/, history.jsonl, session_index.jsonl, shell_snapshots/, sqlite/, *.sqlite*, packages/, .tmp/, node_repl/, ambient-suggestions/, attachments/, computer-use/, process_manager/, memories/, and plugin install staging.
  • Make full-sync subdirectory explosion respect built-in and user exclude patterns before queueing subpath jobs.
  • Change missing trash retention default to 7 days to match the PRD.
  • Add configurable trash caps in preferences: 5 GiB default max size and 512 default max buckets, with 0 disabling each cap.
  • Extend TrashJanitor to remove settled empty buckets and purge oldest settled buckets when size/count caps are exceeded.
  • Count hidden files during trash sizing so .rootfs.img.* backup payloads are included in cap enforcement.

Validation

  • CLAUDESYNC_DISABLE_SINGLE_INSTANCE=1 xcodebuild -scheme ClaudeSync -configuration Debug -destination 'platform=macOS' -only-testing:ClaudeSyncTests/TrashJanitorTests test
  • CLAUDESYNC_DISABLE_SINGLE_INSTANCE=1 xcodebuild -scheme ClaudeSync -configuration Debug -destination 'platform=macOS' -only-testing:ClaudeSyncTests/SyncTargetTests -only-testing:ClaudeSyncTests/FileSyncActorTests/testFullSyncExplode_skipsCodexRuntimeSubdirs_v1_3_5 test
  • CLAUDESYNC_DISABLE_SINGLE_INSTANCE=1 xcodebuild -quiet -scheme ClaudeSync -configuration Debug -destination 'platform=macOS' test

Full suite passed after the final follow-up patch.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 8, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b49a78b-34e9-4e68-85da-3ea23bfcdc92

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-trash-retention-cap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces configurable trash retention and size/count caps to the TrashJanitor to prevent disk exhaustion in high-churn environments, updating the default retention period from 30 to 7 days. It also refactors the FileSyncActor to skip excluded subdirectories during full-sync explosions and adds local-agent VM images and derived caches to the claudeAppSupport exclude patterns. Feedback on these changes highlights a potential data loss issue where a failed directory usage check could lead to premature bucket deletion, and suggests refining the file count logic to ignore empty subdirectories so they do not block cleanup.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +131 to +138
let usage = directoryUsage(url) ?? (bytes: 0, files: 0)
buckets.append(Bucket(
url: url,
name: name,
mtime: mtime,
size: usage.bytes,
fileCount: usage.files
))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If directoryUsage returns nil (e.g., due to transient I/O or permission errors), treating it as (bytes: 0, files: 0) will set bucket.fileCount to 0. This causes isEmptyAndSettled to evaluate to true, leading to the premature deletion of the entire bucket and potential data loss. Skipping the bucket when usage cannot be determined is a much safer approach.

            guard let usage = directoryUsage(url) else {
                logger.warning("TrashJanitor: failed to get usage for \(url.path)", category: "trash")
                continue
            }
            buckets.append(Bucket(
                url: url,
                name: name,
                mtime: mtime,
                size: usage.bytes,
                fileCount: usage.files
            ))

Comment on lines +206 to 222
private func directoryUsage(_ url: URL) -> (bytes: Int64, files: Int)? {
guard let it = fm.enumerator(at: url,
includingPropertiesForKeys: [.totalFileSizeKey],
options: [.skipsHiddenFiles]) else {
options: []) else {
return nil
}
var total: Int64 = 0
var files = 0
for case let fileURL as URL in it {
let values = try? fileURL.resourceValues(forKeys: [.totalFileSizeKey])
if let bytes = values?.totalFileSize {
total += Int64(bytes)
}
files += 1
}
return total
return (total, files)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The fm.enumerator returns all descendants, including directory entries. If a bucket contains empty subdirectories, files will be greater than 0, which prevents the bucket from being recognized as empty and cleaned up. Only counting regular files ensures that empty directory structures are correctly swept.

    private func directoryUsage(_ url: URL) -> (bytes: Int64, files: Int)? {
        guard let it = fm.enumerator(at: url,
                                     includingPropertiesForKeys: [.totalFileSizeKey, .isDirectoryKey],
                                     options: []) else {
            return nil
        }
        var total: Int64 = 0
        var files = 0
        for case let fileURL as URL in it {
            let values = try? fileURL.resourceValues(forKeys: [.totalFileSizeKey, .isDirectoryKey])
            if values?.isDirectory == true {
                continue
            }
            if let bytes = values?.totalFileSize {
                total += Int64(bytes)
            }
            files += 1
        }
        return (total, files)
    }

@ComBba ComBba changed the title [codex] fix trash churn safeguards [codex] fix trash and runtime sync churn Jun 8, 2026
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.

rsync trash 무한 누적: 자동 purge 미작동 + vm_bundles 동기화로 17일간 13GB 적재

1 participant