Fast forking is useful only if branches are also cheap and safe to resolve. An agent can explore three fixes in parallel, but eventually the runtime must answer a harder question: what, exactly, should survive?
The answer is not always "merge the branch." A branch may contain a good source-code patch but a broken development server. It may have a correctly migrated local database but an editor process whose memory should not replace the user's. It may have sent a remote request that no local operation can reverse. Treating all of this as one blob produces either too little state, losing useful runtime context, or too much state, overwriting unrelated concurrent work.
Blog 3 completed the fork boundary across GUI and network state. Blog 4 turns the arrows around. We will separate discard, rollback, whole-workspace promotion, and selective merge, then examine how the current Gensee runtime performs Git and filesystem reconciliation without pretending that arbitrary process memory is mergeable.
Discard preserves none of a branch's state. Rollback resumes one trajectory from an earlier recorded version. Promotion keeps a selected branch as the new complete live workspace. Merge copies selected persistent artifacts into the continuing source. The correct operation depends on whether we want no state, historical state, all live state, or only selected durable state.
A branch is not a patch
Git makes one class of convergence familiar. A branch records changes to a file tree relative to a common ancestor, and a merge reconciles those changes. A live workspace branch is larger. It includes files, process trees, anonymous memory, GUI state, local services, namespace identity, open descriptors, and connections to systems outside the workspace.
These state classes have different algebra. Text files have well-understood three-way merge algorithms. A running process heap does not: two branches can allocate the same virtual address for different objects, hold incompatible locks, and refer to different kernel resources. A GUI compositor has relationships among clients, surfaces, focus, and event queues that cannot be combined window by window without application knowledge. A remote API mutation is not owned by either local branch at all.
| Resolution | What survives | What happens to the source | Typical use |
|---|---|---|---|
| Discard | No branch-local runtime state | Continues unchanged | A failed or inferior candidate |
| Rollback | An earlier coherent workspace version | Only the selected trajectory moves backward | Retry after a bad intermediate action |
| Promote | The selected branch's complete live state | The old source is retired or replaced | Best-of-N with one winning trajectory |
| Merge | Selected persistent artifacts | Keeps its processes, memory, GUI, and I/O identity | Bring code or files into concurrent human work |
Need the branch's complete live runtime? Promote it. Need only selected artifacts? Merge them. Need none of it? Discard it. Need another attempt from history? Roll back.
Discard is a lifecycle operation, not "rm -rf"
A speculative branch may hold CoW references to memory pages and filesystem layers shared with siblings and ancestors. Discarding it means stopping its processes, removing its private namespace resources and writable filesystem delta, releasing shared references, and making the branch unavailable for future execution.
The control plane should still retain lineage: which source created the branch, which task it attempted, why it was rejected, and what tests or policy decisions occurred. The current Gensee integration therefore marks a fork as discarded before delayed cleanup removes its container and attached terminal pane. The historical record remains even though the runnable environment does not.
This ordering handles a small distributed-systems problem. When the lifecycle request originates inside the branch being destroyed, the runtime must deliver and flush the success response before tearing down the caller. Container-mediated cleanup waits for an authenticated acknowledgement. If acknowledgement is lost, cleanup fails closed and leaves the environment running for explicit recovery.
Rollback chooses history; it does not synthesize state
Rollback moves one trajectory to an earlier recorded version without affecting sibling branches. The key word is coherent: process registers, memory, files, GUI state, and local I/O relationships must all correspond to the same version. Restoring only the files beneath a still-running process would produce a mixture that never existed.
The TClone abstraction exposes rollback over the workspace version tree. In the current public Gensee workflow, branch resolution centers on merge, promotion, and discard rather than an arbitrary in-place run rollback command. A failed child can be discarded while its still-running source remains at the earlier point, or a new child can be forked from a retained ancestor. That distinction matters: the system design supports historical workspace versions, while the current orchestrator presents a smaller lifecycle surface.
Rollback also stops at the workspace boundary. If the failed trajectory already pushed a Git branch, sent an email, or modified a remote database, returning local state to an earlier version does not return the remote system with it.
Promotion keeps the whole winning runtime
Promotion is the simplest correct operation when one candidate should win wholesale. Instead of trying to combine its memory and GUI with the source, the system makes that candidate the new main workspace and retires the old source.
This preserves the branch's running development server, installed packages, warmed caches, editor and browser state, local database, process memory, and namespace-local identity as one coherent unit. It is a natural match for best-of-N or beam search when only one trajectory needs to continue.
The TClone design also describes volatile-state promotion at process granularity by replacing a process address space with the selected agent container's address space. The current Gensee integration takes a coarser whole-container path. Its run switch operation rewrites the selected fork's lifecycle context so it becomes the source, transfers the host-control endpoint, ends the previous source, and lets the promoted workspace create and resolve new descendants.
Promotion avoids a state merge, but it does not avoid a product decision. If a human continued editing the old source while the agent branch ran, those human changes do not magically appear in the winner. They must first be reconciled as persistent artifacts, or the user must explicitly accept retiring them with the old source.
Promotion is replacement, not reconciliation. It keeps one internally coherent world instead of constructing a hybrid from two live worlds.
Merge preserves the source runtime
Selective merge is for the opposite case: the source should keep running, but one or more durable branch artifacts should come back. In a coding workflow, this commonly means source files, tests, configuration, generated assets, or selected non-Git workspace files.
The current Gensee runtime exposes three merge scopes. The default --git scope transfers repository changes. --filesystem considers persistent changes under the workspace. --paths narrows that filesystem merge to explicit paths. None of these scopes merges process memory, running tasks, GUI state, sockets, /proc, /sys, /dev, /run, or /tmp.
# Validate without changing the source.
gensee-tclone run merge <fork> --into <source> --git --dry-run
# Merge repository changes, the default scope.
gensee-tclone run merge <fork> --into <source> --git
# Reconcile all persistent workspace files or selected paths.
gensee-tclone run merge <fork> --into <source> --filesystem
gensee-tclone run merge <fork> --into <source> --paths /workspace/src /workspace/package.json
These commands expose the mechanism. In the normal agent flow, Codex summarizes the candidate and asks the user to choose merge, promotion, or discard; the lifecycle command runs only after that explicit choice.
The Git path uses the fork point as its base
At fork time, Gensee records the source repository's HEAD. When the branch finishes, the Git merge path computes a binary-capable patch from that commit to the branch's current working tree. This includes commits made after the fork point, staged and unstaged changes, deletions, and untracked files. Runtime telemetry and local Gensee database files are excluded.
The patch then crosses into the source container. A dry run uses git apply --check; a real merge uses git apply. If the patch no longer applies because the source changed the same hunk or path incompatibly, Git reports the conflict before the candidate is marked merged.
This is intentionally a repository-level operation. It transfers code history as a content delta, not the branch's package installation, process tree, terminal scrollback, or warmed language server. After merge, the source may need to install dependencies, restart a server, or rerun tests in its own runtime.
The filesystem path is a conservative three-way merge
Not every useful artifact belongs to Git. A coding agent may produce a test database, build report, generated binary, downloaded fixture, or application file elsewhere under the workspace. For these cases, Gensee uses the TClone overlay layout as a merge data structure.
The fork's immutable lower layer is the common base. Its upper layer enumerates paths the fork created, modified, or deleted. The continuing source provides the third view. For each changed path, the runtime computes a signature that captures type, mode, and content for regular files, or the target for a symbolic link.
If the source path still matches the base, only the fork changed it, so the fork result is eligible. If the source already matches the fork, the path has converged. If the source differs from both base and fork, both trajectories changed the path differently, so Gensee reports a conflict and stops before modifying the source.
This algorithm is stricter than Git's text merge. Two branches editing different lines of one text file may be mergeable by Git, but a generic filesystem merge sees one path whose contents diverged in both branches and stops. That conservatism is useful because the file might be a SQLite database, image, package archive, or application-specific binary for which line merging would be meaningless or corrupting.
Applying a filesystem merge is its own transaction
Conflict detection answers whether a merge plan is acceptable. Applying the plan introduces a second problem: a failure after deleting one destination but before copying its replacement must not leave the source half-merged.
Gensee first copies all eligible fork upserts into a private staging tree inside the source container. During apply, each existing destination moves into a backup tree and receives an applied marker. The runtime then performs planned deletions, directory creation, and file or symbolic-link upserts. A shell trap remains active through the operation. If any step fails or the process is interrupted, destinations are restored from backup in reverse order. Only after every change succeeds are the trap and temporary transaction tree removed.
This gives the selected path set recoverable all-or-nothing behavior for apply failures. It does not provide database-style isolation from concurrent source writes. The source agent should therefore be idle, stopped, or at a known checkpoint while a merge is validated and applied.
A generic runtime may safely move whole paths whose ancestry is unambiguous. Understanding and combining the internal meaning of arbitrary file formats belongs to Git or to the application that owns the format.
Why live process memory is promoted, not merged
It is tempting to imagine a page-level equivalent of file merge: compare two process heaps to their shared CoW ancestor and copy changed pages. The memory pages are easy to identify. Their meaning is not.
A changed page can contain half of an object whose other half is on an unchanged page. A pointer can target an allocation that exists only in one branch. A lock word can describe a thread that the other branch never created. File descriptors, futex waiters, timers, shared memory, and kernel objects can disagree with the copied bytes. Even two disjoint page writes can violate an application invariant when combined.
For this reason, TClone uses CoW to fork memory but does not claim a general semantic merge for independently mutated address spaces. If volatile state matters, select one coherent branch through promotion. If only durable output matters, merge the files and let the source applications reload or restart.
| State class | Useful convergence rule | Why |
|---|---|---|
| Git repository files | Patch from the recorded fork commit | Git understands content ancestry and text conflicts. |
| Non-Git workspace files | Path-level three-way merge | The runtime can detect unambiguous creates, updates, and deletes without guessing file semantics. |
| Database files | Application export/import, or promote | Byte-level mixing can violate database and WAL invariants. |
| Installed system packages | Re-run installation, or promote | Current filesystem merge is confined to the workspace and excludes system state. |
| Processes, memory, GUI, local services | Promote one coherent workspace | Generic cross-branch semantic merge is undefined. |
| Network sessions | Reconnect under the surviving workspace identity | Transport state and remote ownership cannot be combined locally. |
| External effects | Separate approval or application commit protocol | The remote system is outside workspace rollback. |
One coding task, four outcomes
Suppose a source workspace contains Codex, an editor, a browser, and a development server. A fork updates application code, adds a package dependency, installs the package, migrates a local test database, and verifies the result in the browser.
A Git merge returns the source changes and package manifest while preserving the source's running processes. The source still needs to install the dependency, restart the server, and rerun the migration. A selected-path filesystem merge could also return a generated report, but should not byte-merge a live database. Promotion keeps the branch's installed package, migrated database, warm server, browser, and memory together, while retiring the old source. Discard keeps none of the fork. Rollback abandons the failed trajectory and resumes from a recorded ancestor for another attempt.
The "best" operation therefore depends on the artifact. A good code diff does not imply a good runtime. A good runtime does not imply that concurrent source edits should be overwritten. The resolution UI has to show enough evidence for the user or policy to choose deliberately.
Commit is also an authority boundary
Making branch creation cheap encourages wider speculation, which makes commit policy more important. A branch should not be able to declare itself the winner and overwrite the source merely because its agent process asks.
In the current integration, the source can inspect direct child branches and read their changed files, test results, and summaries. Codex presents merge, promote, or discard as explicit choices. The container-to-host bridge checks that a later user prompt recorded the same unconsumed choice before it accepts the lifecycle operation. Capabilities are scoped by lineage: a branch can resolve only itself against its direct source, not select an unrelated workspace.
This does not prove that the branch is correct. It makes the authority transition explicit, reviewable, and attributable. Conflict checks protect state consistency; approval protects ownership.
What the current public runtime supports
The public TClone runtime provides the live container fork substrate. Gensee Crate supplies the agent-facing lifecycle and reconciliation layer described here.
| Capability | Current behavior |
|---|---|
| Git merge | Binary-capable patch from recorded fork HEAD; supports dry-run validation. |
| Filesystem merge | Workspace-confined full or selected-path merge using overlay base/delta metadata, path conflict detection, staging, backups, and rollback on apply failure. |
| Promotion | The selected fork becomes the new source; lifecycle context and host control move with it; the old source ends. |
| Discard | The fork is marked discarded, then cleaned up after response acknowledgement; history remains. |
| Rollback | Part of the TClone version-tree abstraction; the current user workflow retains ancestors and resolves children rather than exposing arbitrary in-place rollback as a lifecycle command. |
| Not merged | Live memory, process state, GUI state, sockets, pseudo filesystems, system paths outside the workspace, and external side effects. |
TClone evaluates the cost and scalability of branch creation, which is the latency-critical path for speculative agents. It does not present a separate merge-throughput evaluation. The merge mechanisms in this post should therefore be read as implementation semantics and failure handling, not as a claim about a reported merge benchmark.
Fork shares bytes. Resolution preserves meaning. That requires type-specific merge rules, a whole-state promotion path, and an explicit authority transition.
Where this leaves the series
We now have the complete workspace lifecycle: choose a container-sized boundary, fork process and memory state, preserve files and page cache, reconstruct GUI and network identity, then resolve the resulting branches without inventing a universal merge operator.
Blog 5 will move up one layer and follow this lifecycle through the Codex integration: how the agent proposes a fork, how work continues inside the cloned session, how the source compares parallel candidates, and how explicit user approval becomes a merge, promotion, or discard operation.
Read the runtime and orchestration code. The public repositories contain the live-container fork substrate, Git and filesystem merge scopes, conflict checks, promotion flow, discard cleanup, and Codex lifecycle integration discussed here.
Explore GenseeAI/os4agent on GitHubFrequently asked questions
What is the difference between merge and promotion?
Merge transfers selected persistent artifacts into the continuing source workspace. Promotion makes the selected branch the new main workspace and retires the old source. Use merge when the source runtime should survive; use promotion when the branch's complete live state should survive.
How are filesystem merge conflicts detected?
For each path changed in the fork's overlay upper layer, Gensee compares the source path with the immutable fork base and the fork result. If the source matches the base, the fork change is eligible. If it matches the fork, the path is already converged. If it matches neither, the merge stops and reports a conflict.
Does filesystem merge combine different lines in one text file?
No. Generic filesystem merge is conservative at path granularity. Use the Git scope for repository content that benefits from Git's text and binary patch semantics. Application-specific formats such as databases should use their own export, migration, or transaction mechanisms.
Can two branches merge their process memory?
Not generically. Page-level differences do not capture application invariants, object graphs, locks, kernel resources, or I/O relationships. TClone shares memory with CoW during fork, but independently mutated runtime state is selected through promotion rather than combined.
Can the source keep changing while merge runs?
It can continue while the branch is evaluated, but merge should occur while the source agent is idle, stopped, or at a known checkpoint. Otherwise a concurrent write can race with validation or apply.
Can discard or rollback undo remote actions?
No. Local workspace resolution cannot undo an email, payment, deployment, remote API mutation, or Git push already accepted by another system. External effects require separate egress policy, approval, idempotency, delayed release, or a remote transaction protocol.
Further reading
Read more about TClone, the accompanying systems note, the open-source GenseeAI/os4agent runtime, and the Gensee Crate TClone integration documentation. Earlier posts cover the system boundary, process trees and memory CoW, files and the Linux page cache, and GUI and network forking.