The first three posts in this series established a container-sized branch and then followed its process and filesystem state. Blog 0 chose the system boundary. Blog 1 covered process trees and anonymous memory. Blog 2 covered files and the Linux page cache.
Those mechanisms are enough for a headless batch job. A coding-agent workspace is often more demanding. It may contain a browser logged into a test system, a desktop editor, a compositor tracking windows and focus, a development server listening on a port, local browser-to-helper connections, and long-lived sessions to remote APIs. Forking only the application processes would preserve fragments of that state while losing the relationships that make it usable.
This post asks where GUI and network state should live, what can be cloned safely, and what must be recreated or governed because it crosses into a system the workspace does not control.
TClone puts the GUI compositor inside the workspace container, so windows, focus, cursor, clipboard, and input state cross the fork boundary with the applications. It keeps rapidly rewritten display buffers branch-local. Each branch receives a fresh network namespace and independent host-port mappings. Local connections can be reconstructed in the full design, while connections to remote systems must reconnect. Remote mutations such as a deployment, payment, email, or Git push are not snapshot state and require a separate policy boundary.
The branch boundary ends where another system begins
A useful first step is to classify state by ownership rather than by data type. A TCP socket may connect two processes inside one workspace, or it may connect a browser to a bank. A shared-memory region may hold a mostly static application index, or a framebuffer rewritten every frame. The mechanisms should differ because the ownership and mutation patterns differ.
| Class | Examples | Fork behavior |
|---|---|---|
| Versioned workspace state | GUI applications, compositor, window geometry, focus, clipboard metadata, local daemons | Capture and reconstruct inside each branch. |
| Stable shareable state | Mostly unchanged process memory, browser code pages, application files | Share with copy-on-write where profitable. |
| Write-hot branch-local state | Framebuffers and frequently rewritten display shared memory | Give each branch private storage immediately. |
| Rebound identity | Network namespace, host port, HTTPS display stream, remote TCP session | Create new branch identity and reconnect. |
| External effect | Email, payment, deployment, production API write, Git push | Block, allowlist, defer, or require approval. Local rollback cannot undo it. |
This taxonomy prevents a common mistake: treating everything reachable from a process as if it were owned by that process. A workspace owns its half of a connection. It does not own the remote peer, the peer's database, or the consequences of a request the peer has already accepted.
Why a host display server breaks the snapshot boundary
Many containerized desktop setups put applications in the container while keeping the X11 or Wayland display server on the host. That is convenient for ordinary isolation, but it splits one logical desktop across two lifecycle domains.
The browser and editor processes may live inside the container. Window placement, focus, cursor state, input routing, clipboard ownership, and display buffers may live in the host compositor. Capture only the container and half of the desktop survives. After restore, an application can believe it owns a surface that the compositor no longer knows about, or the compositor can retain an object whose client identity has changed.
The problem is not graphical pixels alone. GUI applications and compositors coordinate through sockets, shared-memory buffers, object identifiers, and event queues. Those relationships need one consistent point-in-time boundary.
If restoring the applications without restoring a component can leave the desktop inconsistent, that component belongs inside the workspace branch.
Put the entire GUI stack inside the workspace
TClone runs the GUI stack inside each workspace container. Its desktop setup uses Webtop with Selkies: GUI clients and the Wayland compositor run in the container, and the rendered desktop is exported to the user or computer-use agent over HTTPS.
This changes the failure boundary. The compositor is now an ordinary workspace process with memory, file descriptors, IPC endpoints, and child relationships that can be captured together with the applications. When the workspace forks, each branch reconstructs its own compositor and clients. Each branch therefore has its own windows, focus, cursor, clipboard, and input stream.
The HTTPS display stream is deliberately outside that captured desktop state. It is a transport used to observe and control a branch. After the fork, the source and child expose separate streams. An input event intended for one branch no longer enters a host-global queue shared by both.
This is especially important for computer-use agents. Two branches can start from the same open browser tab and then click different controls without racing over one cursor or clipboard. The semantic desktop state is cloned; future interaction becomes branch-specific.
Why TClone does not CoW every GUI byte
Copy-on-write is valuable when most inherited pages remain unchanged. The source and child share a physical page until one writes it. The first write then incurs a fault and creates a private copy. As Blog 1 explained, this is a strong fit for code, heaps, indexes, and other large regions whose changes are sparse relative to their size.
A display buffer has the opposite workload. A compositor or GUI client can rewrite large portions of it every frame. Sharing the initial pages would save memory only briefly. The next render would trigger many CoW faults and copies, after which the pages would be private anyway.
TClone therefore treats rapidly mutating GUI shared memory as genuinely branch-local. The application and compositor still benefit from CoW for their larger, less volatile memory regions, but framebuffers and similar write-hot buffers start private. This is not a correctness concession. It is a choice to avoid a sharing mechanism where the expected reuse is lower than its fault and bookkeeping cost.
Share state according to its expected write pattern, not merely because it happens to live in memory. CoW suits stable inherited state; immediate privatization suits buffers rewritten continuously.
What this costs on real desktop tasks
Microbenchmarks can isolate clone time or page-fault cost, but a computer-use agent experiences the entire task path. OSWorld evaluates multimodal agents on open-ended tasks in real computer environments. The TClone evaluation runs Agent S3 on these tasks and groups them into browser, office, creative/communications, and multi-application workloads.
The left panel below reports end-to-end task latency, not just time spent forking. TClone is lowest in all four categories. The difference is visible even in long workflows because branching remains on the execution path: a browser task takes roughly 150 seconds with TClone versus 250 seconds with CRIU and 270 seconds with KVM, while multi-application tasks take roughly 590, 890, and 1,050 seconds, respectively.
The right panel reports memory footprint on a logarithmic scale. TClone is tied with CRIU for browser tasks and lower for the other groups, while KVM consumes substantially more memory throughout. The gap grows for office and multi-application tasks, which keep more application, GUI, and filesystem state live at once. TClone can leave write-hot display buffers private and still retain most of the memory benefit by sharing larger unchanged anonymous-memory and read-mostly filesystem regions.
This is a whole-system result, not a measurement of the GUI mechanism in isolation. It reflects how process reconstruction, CoW memory, filesystem sharing, GUI ownership, and runtime orchestration combine during complete desktop-agent tasks.
A network fork needs a new identity
Networking introduces two separate problems. First, sibling branches need isolated local address spaces so their loopback services and listening ports do not collide. Second, an established connection has a peer outside the workspace that did not participate in the fork.
Linux network namespaces solve the first problem. A network namespace owns interfaces, routes, firewall state, and the TCP/UDP port space. TClone creates a fresh namespace for the branch and configures its network separately. The source and child can both run a development server on container port 3000, because each namespace has its own port table and loopback interface.
Published host ports need another layer of identity. The public runtime preserves the container-side port while allocating a different host-side port for the branch. For example, the source might expose host port 3101 to container port 3000, while the child exposes host port 43827 to its own port 3000. Code inside either workspace keeps using the expected local address; the control plane routes users and tools to the correct branch.
If both endpoints belong to the workspace, their relationship can be reconstructed. If the peer is outside the workspace, the branch must negotiate a new relationship.
Internal TCP and external TCP are different problems
Consider a browser connected to a helper daemon over 127.0.0.1. Both endpoints live in the same workspace and both are captured at the same point in time. The branch owns the client socket, server socket, sequence state, and network namespace. In the full TClone design, CRIU's kernel TCP-repair mechanism can restore this internal connection in the child.
Now consider the browser's TLS connection to a remote API. The branch contains only one endpoint. Duplicating the local TCP sequence numbers does not cause the server to recognize two independent clients. If source and child emitted different byte streams under one inherited remote identity, packet ordering and connection ownership would become ambiguous. Application-level side effects could also be replayed accidentally.
External connections therefore close at the fork boundary and reconnect independently. The browser, database client, package manager, or agent tool establishes a fresh transport from the branch's new network identity. Higher-level application state may survive: a browser can still have cookies and a logged-in profile, for example. But the remote service decides whether those credentials remain valid and whether concurrent sessions are permitted.
What the public runtime does today
The current open-source os4agent Podman path takes a deliberately conservative approach. Its --tfork-tcp-close option defaults to true. During capture, established TCP sockets are represented as closed in the clone. Listening sockets remain available, and applications are expected to reconnect their outgoing sessions after restoration.
That default applies to established connections broadly, including local ones. It does not yet classify every socket by whether both endpoints are internal. Setting --tfork-tcp-close=false is also not a switch that automatically preserves established sessions; it falls back to CRIU's normal refusal unless an established-TCP restore path is configured.
# Current public default: restore established TCP sockets as closed.
podman container clone source branch --tfork-tcp-close=true
# Listening services survive in the branch's fresh network namespace.
# Clients reconnect using branch-specific network and host-port identity.
This distinction matters when reading a system design beside an evolving implementation. The architecture gives internal connections a correctness path through TCP repair and treats external connections as reconnectable. The public runtime currently chooses the simpler conservative behavior for all established TCP sockets.
Closing TCP is necessary, but not sufficient
Reconnection answers a transport question: which sequence numbers and address tuple does the branch use? It does not answer the application question of what should happen next.
A reconnecting browser may resume a WebSocket subscription. A database client may reopen a pool. An HTTP library may retry a request whose response was lost during the fork. If the request was a read, retry is usually straightforward. If it charged a credit card or triggered a deployment, retry can duplicate an external effect unless the protocol provides an idempotency key or transaction boundary.
UDP and QUIC make the same ownership issue visible in different forms. A network namespace can reproduce local sockets and addresses, but the remote protocol endpoint still has its own state and timeout behavior. No local snapshot can force an outside service to rewind with the branch.
External effects are not workspace state
A container branch can be discarded. The files it wrote disappear, its processes stop, and its local database can return to the pre-fork point. An email already accepted by a mail provider does not disappear. Neither does a merged pull request, cloud resource deletion, production deployment, payment, or message sent to a customer.
Transactional workspace semantics therefore require an explicit external-effects policy. Depending on the task, a branch may run with no egress, connect only to allowlisted read-only services, buffer selected writes for later release, require human approval before a mutation, or use application-level test environments and idempotency controls.
This policy does not make a remote system part of the local transaction. It prevents the branch from confusing reversible local speculation with an irreversible external action.
| Agent action | Local state | External consequence | Useful control |
|---|---|---|---|
| Load documentation | Browser cache, history, cookies | Remote read and access log | Allowlisted egress or proxy logging |
| Run a local test app | Dev server, browser tab, local database | None if dependencies remain local | Independent network namespace and ports |
| Call a staging API | Client state and response cache | Staging data mutation | Scoped credentials and idempotency keys |
| Push a Git branch | Local refs and objects | Remote repository changes | Deferred release or explicit approval |
| Deploy to production | Build artifacts and CLI state | Production changes | Blocked by default; separate deployment gate |
If discarding the branch cannot undo an action, that action is outside the workspace transaction and needs its own commit protocol.
Following one coding-agent fork end to end
Suppose a coding agent has an editor, browser, local API server, and test database running in one live workspace. The browser is open to http://127.0.0.1:3000 and also has an authenticated tab for a remote source-control service.
- Freeze: TClone establishes a consistent point while process, GUI, filesystem, and socket state are captured.
- Reconstruct: The child receives the process tree, compositor, browser, editor, local database, files, and private namespace resources.
- Share selectively: Stable application and file-backed memory remains CoW-shared; write-hot display buffers are private.
- Rebind networking: The branch receives a fresh network namespace. Its local server can still listen on
:3000, while any published host port is remapped. - Reconnect transport: In the current public path, established sockets return closed. The browser and other clients reconnect.
- Apply egress policy: Local testing proceeds. A remote push or production mutation waits at a separate gate.
The result is not a pixel copy of a desktop and not a packet replay. It is a new live workspace that begins from the same semantic local state and then acquires its own display, interaction, and network identity.
What remains outside the model
Moving the software compositor into the container creates a strong boundary for browser- and desktop-based agent work, but it does not automatically virtualize every device. Direct GPU contexts, cameras, microphones, USB devices, hardware security keys, and host audio stacks carry driver and hardware state that may not be checkpointable or safely shareable. They need explicit virtualization, branch-local reinitialization, or a policy that excludes them from the fork.
Authentication also deserves careful wording. A branch can inherit local credentials, cookies, browser profiles, and tokens because they are local files or memory. That does not duplicate the authority of the issuing service. The service can expire a session, reject concurrent use, bind a token to a device, or record both branches under one account. Local state is cloneable; remote authority remains remote.
Clone local semantic state. Privatize write-hot buffers. Recreate volatile transport. Rebind branch identity. Gate effects the workspace cannot roll back.
Where this leaves the series
The workspace can now branch across process, memory, filesystem, GUI, and local network state without pretending it has cloned the outside world. Blog 4 will turn from fork to convergence: what it means to promote, discard, roll back, or merge a branch when different state classes need different conflict and commit rules.
Inspect the implementation details. The open-source runtime includes the Podman clone path, network-namespace setup, port remapping, TCP-close option, CRIU changes, and Webtop image used in this post.
Explore GenseeAI/os4agent on GitHubFrequently asked questions
Why run the GUI compositor inside the workspace container?
A GUI application's complete state spans the application and the compositor. Window objects, focus, cursor, clipboard, input routing, event queues, and display buffers must agree. Putting the compositor inside the workspace lets those relationships be captured and reconstructed with the application processes.
Are framebuffer pages shared between branches?
TClone keeps rapidly rewritten shared-memory display buffers branch-local. These pages would become private almost immediately under CoW, so sharing them would add page faults and copy work with little sustained memory benefit. Less volatile application memory can still remain CoW-shared.
What happens to established TCP connections?
The full design distinguishes internal connections, whose endpoints both belong to the workspace, from external connections to remote peers. Internal TCP can be reconstructed with kernel TCP repair; external sessions reconnect. The current public os4agent runtime conservatively enables --tfork-tcp-close=true by default for established TCP sockets broadly, while preserving listening sockets.
Why do branch development servers not collide on port 3000?
Each branch has its own Linux network namespace and therefore its own port space and loopback interface. Both can listen on container port 3000. When exposed to the host, the runtime allocates a separate host-side port for each mapping.
Can rollback undo an external side effect?
No. Rollback controls state inside the workspace. Once a remote service accepts an email, payment, deployment, repository push, or API mutation, that service owns the result. Such actions need egress controls, approval, delayed release, or an application-level transaction protocol.
Does a forked browser remain logged in?
Its local profile, cookies, storage, process memory, and tabs can be part of the branch. Its network connections are re-established, and the remote service still decides whether the inherited token is valid and whether concurrent sessions are allowed.
Further reading
Read more about TClone, the accompanying systems note, the open-source GenseeAI/os4agent repository, the OSWorld benchmark, CRIU's documentation for TCP connection checkpoint and restore and the --tcp-close option, and the Selkies and Webtop projects used to place a web-accessible desktop inside the container boundary.