In Blog 0, we chose the container as the useful boundary for a live coding-agent workspace. In Blog 1, we followed TClone through process-tree reconstruction and anonymous-memory copy-on-write.
This post covers the file-backed half of the workspace. A coding agent does not work against an empty root filesystem. Its branch may inherit millions of source files and dependency files, checked-out repositories, compiler artifacts, package caches, database files, and binaries that were already read into memory. Forking all of that efficiently requires us to ask two separate questions: which bytes are shared on storage, and which bytes are shared in RAM?
A Btrfs snapshot shares unchanged disk extents, but the source and snapshot normally have distinct inode and address_space objects. Linux can therefore cache the same file bytes once for each branch. TClone closes that gap in two related ways: a kernel-assisted path that shares eligible cached folios through immutable CoW layers, and an integrated OverlayFS path in which siblings read from one stable lower filesystem while keeping writes in private upper directories.
One file has three layers of state
Suppose a branch reads page 12 of node_modules/typescript/lib/typescript.js. There are three different identities involved in that apparently simple operation.
| Layer | What identifies the data | What CoW can share |
|---|---|---|
| Filesystem namespace | A path resolves to an inode in a particular mount and snapshot. | Snapshot metadata can cheaply create a new tree that initially names the same contents. |
| Persistent storage | An inode's logical range maps to one or more physical extents. | Btrfs reflinks and snapshots can reference the same extent until a writer changes it. |
| Page cache | Linux indexes cached folios by an address_space and page offset. |
Ordinary filesystem CoW does not make separate address_space objects share a cache entry. |
The page cache is the kernel's in-memory copy of recently accessed file data. On a cache hit, a process can read bytes from RAM instead of issuing storage I/O. A cache entry belongs to a file mapping, represented by the kernel's address_space, and is indexed by file offset. Modern kernel code usually manipulates these entries as folios, which may contain one or more pages.
That ownership rule is important. Two snapshot files may refer to the same physical extent on disk while still appearing as different VFS inodes with different mappings. If both are read, each mapping can acquire its own folio containing identical bytes.
One shared disk extent does not imply one shared page-cache folio. A storage-space measurement can look excellent while every live branch quietly duplicates hot, read-mostly file data in RAM.
Why snapshots solve only the disk half
Btrfs snapshots are a strong starting point. Creating a writable snapshot does not immediately copy every file. The source and branch initially reference the same extents, and Btrfs allocates new extents only when one side changes data. This is exactly the right persistent-storage behavior for a branch that may touch only a small fraction of a large workspace.
But a live agent workspace also has a hot working set. Package metadata, shared libraries, compiler binaries, language-model tooling, repository indexes, and recently built outputs may already be resident in the host page cache. A new Btrfs snapshot gives those files snapshot-specific VFS identities. Reading them through the branch can populate new page-cache entries even when the underlying extents are still shared.
At small scale, the duplicate cache may be harmless. At high fan-out, it changes the economics. If 16 branches repeatedly read the same gigabytes of dependencies, block-level CoW protects storage capacity while memory pressure can still rise with branch count. Reclaim then discards useful pages, and later reads must fetch them again.
Why stacking OverlayFS is not a complete answer
OverlayFS offers another natural branching model. A read-only lower directory contains inherited files, while an upper directory contains the branch's changes. Reads search the upper first and then lower layers; writes copy a lower file up before changing it. Deletions are represented by whiteouts so the lower object stays hidden inside that branch.
The trap is recursive branching. If every child uses its parent workspace as a new lower layer, a branch-from-a-branch creates a deeper lookup chain. An inherited path may require checking many layers before the kernel finds it. The branch remains space-efficient, but lookup work grows with history depth.
The following experiment measures per-operation latency on a 1 MB contiguous file across branch depths from 0 to 50. Operations on a page already materialized in the branch stay flat: read_rw remains near 1.4 ms, and write_rw remains near 2.5 ms, because neither operation traverses ancestor layers. Only the cold shared read, read_ro, walks the lower-layer chain.
read_ro, the cold shared read, grows with layer distance: from about 1.3 ms at depth 0 to about 5.7 ms at depth 50. The branch-local read_rw and write_rw paths remain roughly flat. Source: Figure 12 of the TClone paper.The shared-read increase is approximately linear, with each additional layer adding lookup work rather than creating nonlinear compounding. That is still the structural problem: OverlayFS inflates every lower-layer read as the union deepens. A deep chain is therefore a poor fit for read-heavy, snapshot-frequent computer-use-agent (CUA) branching.
Design path 1: lazy page-cache CoW
The TClone paper introduces a kernel mechanism for sharing page-cache data across snapshot files with different address_space objects. The implementation in linux-pagecache-cow calls this layer filecow.
At fork time, address_space_fork(new, source) creates a sealed shared layer from eligible source-cache state. The layer is immutable after publication. Each live mapping can then hold branch-private folios while consulting one or more shared ancestors.
A read follows a simple order:
- Look for the page index in the branch's private cache.
- On a miss, walk the immutable shared
filecowlayer or layer chain. - Only after those misses, read the branch's own extent from storage.
When a shared folio satisfies the read, the branch can map the same physical data read-only. When the branch writes, filecow_cow_folio() allocates and copies a branch-private folio. The writer changes its copy; siblings and ancestors keep the earlier contents.
Sharing requires an identity check
Matching inode numbers and offsets is not enough. A source file and snapshot file can diverge after the snapshot. Reusing a cached source folio after the branch's underlying extent changed would return the wrong bytes.
TClone therefore asks Btrfs whether the source and snapshot still map the relevant logical range to the same extent before admitting a folio to the shared layer. If the extents no longer match, that page is excluded; the branch must read its own version. The test turns page-cache sharing from best-effort deduplication into a correctness-preserving operation.
Dirty pages need a stable boundary
A dirty folio contains newer data than persistent storage. A folio under writeback is changing ownership state while the kernel sends it to disk. Neither can be casually published into an immutable point-in-time layer.
The current address_space_fork() path refuses mappings with dirty or writeback state rather than guessing. The surrounding fork protocol must freeze mutation and establish the required clean boundary first. This is one place where a live filesystem fork differs sharply from a normal recovery snapshot: both source and child will resume and accept writes immediately, so the handoff point must be explicit.
A shared cached folio is safe only when it is immutable in the shared layer and still represents the same underlying file extent in every mapping allowed to consume it.
Design path 2: one shared lower, private uppers
There is a second way to avoid duplicate cache identity: make sibling branches read unchanged files through the exact same lower inode. The integrated runtime's --tfork-overlay-btrfs path combines one read-only Btrfs snapshot with branch-private OverlayFS upper directories.
one read-only Btrfs snapshot
snap-ro
/ | \
branch A branch B branch C
upper-A upper-B upper-C
For an unchanged file, every sibling resolves the read to snap-ro. Because the lower file is literally the same inode and address_space, the ordinary Linux page cache already has the desired sharing identity. There is no need to teach separate snapshot mappings to find each other's folios. A branch write triggers OverlayFS copy-up into that branch's upper directory, where both its cache state and persistent changes become private.
This layout also handles recursive forks without indefinitely stacking overlays. When a branch with existing upper-layer changes becomes the parent, the runtime keeps the original stable snap-ro, reflink-copies the parent's upper state into a new child upper, and mounts another one-level overlay. The child inherits the parent's point-in-time changes, but lower lookups do not gain another historical layer.
The two designs are not competing descriptions of one code path. They are two mechanisms for the same higher-level invariant:
Unchanged file data should have one stable cache identity; changed data should acquire branch-local identity only when a branch writes.
What gets copied, and when?
| Operation | Shared state | Private state |
|---|---|---|
| Create a branch | Unchanged Btrfs extents and eligible cached file data | Branch namespace metadata, writable upper or snapshot identity |
| Read an unchanged cached file | An immutable filecow folio or the common lower inode's folio |
Usually no file-data copy |
| Read an unchanged cold file | The loaded data can serve later readers through the shared layer or common lower mapping | Lookup metadata and branch-local descriptors |
| Write a file | Unchanged pages and extents remain shared | The affected folio and storage extent, or an OverlayFS copy-up |
| Delete or truncate | Other branches keep their earlier view | Branch-specific inode changes, whiteout, or truncated extents |
| Fork a branch again | Earlier immutable layers or the same stable lower | A new layer generation or reflink-seeded child upper |
Correctness is stricter than deduplication
Finding two equal byte strings is easy. Sharing them while two filesystems continue to mutate is a lifecycle problem. A complete design has to answer at least six questions.
- Point in time: Which file version belongs to the branch when the source is live again?
- Dirty state: How are dirty and writeback folios quiesced before the shared view is published?
- Extent identity: How does the cache verify that a folio still represents the branch's persistent bytes?
- Mutation semantics: What happens on write, truncate, hole punch, rename, unlink, and mmap-based modification?
- Memory management: Can reclaim, readahead, and page faults operate without violating layer ownership?
- Lifetime: Which branch holds references to a shared layer, and when can that layer finally be collected?
These are kernel and runtime questions, not merely snapshot API questions. The fact that a filesystem exposes a fast clone operation says little about how live file mappings, cache entries, open descriptors, and later branch mutations interact.
What the measurements tell us
The OverlayFS depth experiment isolates one dimension: a branch representation can remain storage-efficient while making inherited reads progressively slower. At depth 50, read_ro is about 4.4 ms slower than at depth 0 for the 1 MB test file. Meanwhile, read_rw and write_rw stay flat because their pages are already branch-local and do not traverse ancestors.
The TClone paper also reports aggregate memory at 16 concurrent clones: about 9 GB for TClone versus about 14 GB for CRIU in that experiment. That gap is broader than the page cache alone. It includes TClone's sharing of unchanged anonymous memory as well as read-mostly file-backed state. The useful conclusion is therefore not that page-cache CoW saves a fixed percentage. It is that a complete live fork must preserve sharing across both anonymous and file-backed working sets.
Workload shape still matters. A repository-wide formatter that rewrites most files will privatize more state than agents mostly reading dependencies and editing a handful of source files. Branch fan-out, cache temperature, file sizes, mmap behavior, and write locality all affect the result.
Why coding-agent workspaces fit this model
Coding workspaces tend to have a large shared base and a thin, unpredictable delta. Sibling agents may read the same compiler, standard library, package graph, repository history, language-server index, and test binaries. One agent edits a parser; another changes a database query; a third only inspects the code. Most inherited file bytes never change in any given branch.
That is the workload CoW is built for. The fork should avoid copying the common base, charge each branch for its actual writes, and preserve a stable view even while other branches continue running. Filesystem snapshots provide the persistent half. Page-cache sharing provides the in-memory half.
For live workspace forking, ask about two CoW planes: disk extents and cached folios. If a design explains only one, its efficiency story is incomplete.
Where this leaves the series
We now have process trees, anonymous memory, file namespaces, storage extents, and file-backed cache state. The next boundary is less cooperative: GUI sessions and networking connect a branch to devices and systems outside the container. Blog 3 will separate state that can be cloned from external side effects that must be isolated, redirected, or governed by policy.
Read the implementation. The open-source tree includes the TClone runtime components and the experimental filecow kernel path discussed here.
Frequently asked questions
Why is a Btrfs snapshot not enough?
Btrfs snapshots share unchanged disk extents through block-level CoW. Source and snapshot files still have distinct VFS mapping identities, so Linux can keep separate page-cache folios containing identical bytes. Disk deduplication does not automatically deduplicate RAM.
What is the Linux page cache?
It is the kernel's in-memory cache of file contents. Cached folios are associated with a file's address_space and offset. Reads can use those folios instead of fetching data from storage again.
What happens when a branch writes a shared file page?
In the lazy filecow path, the kernel copies the shared folio into the writer's private mapping before modification. In the shared-lower OverlayFS path, the write is directed to the branch's private upper layer, with copy-up when needed. Other branches retain the earlier contents.
Why not keep stacking OverlayFS layers?
Repeated branch-from-branch operations can create deep lower-layer chains. Reads of inherited data then search more layers. TClone's shallow layout keeps one stable read-only lower and carries inherited branch changes into a newly seeded private upper.
Does TClone share dirty pages?
The page-cache CoW path does not blindly publish dirty or writeback folios as immutable shared state. It requires a stable point-in-time boundary and checks that relevant Btrfs extents still match before cached data is shared.
Further reading
Read the TClone paper, the accompanying systems note, the open-source GenseeAI/os4agent repository, the Linux kernel's OverlayFS documentation, and the Btrfs subvolume documentation.