Appearance
turbo merge
Combine the changes several sessions made since they were forked from a common point back into one session. Use it when you branched a session with turbo fork and want the work from each branch in one place.
Usage
text
turbo merge [options] <target> <source> [<source>...]
turbo merge --status <target>
turbo merge --continue <target>
turbo merge --abort <target><target> must be stopped. Sources may be running; each is paused only for the moment it takes to record its state and is resumed afterwards.
Description
- Merging is three-way. The merge base is the most recent snapshot every session in the merge has in common, found by walking each session's fork history. Sessions that share no fork point are refused; pass
--baseto name one yourself. - A path only one session changed is applied as it is. A path several sessions changed to the same result is applied once. A path several sessions changed differently is a conflict and goes to the merge driver for that kind of content.
- Nothing in the target changes until the final swap. A preview, an unresolved conflict, a rejected resolution and a failing
--verifyall leave the target exactly as it was. - The swap names the overlay it expects to replace: the target's overlay as the plan was computed against it. If the target was written to in the meantime, the swap is refused and nothing changes, because the result was computed without those writes. Run the merge again to plan against what the target now holds.
- A snapshot is named by what it holds, so its id is
snp-followed by the whole content address, sixty-four hexadecimal digits. Printed output abbreviates it to the first twelve;--format=jsoncarries all of it. - A part of a state unit whose selectors matched nothing in the unit is listed in the brief. That is a note rather than an error: a descriptor cannot know an application's tables in advance, and those entries, if they appear later, merge as the rest of their unit.
- An unresolved conflict leaves a merge workspace under the target's session directory:
merge-state.json, a readablebrief.md, and one directory per conflict holdingbase,ours, onetheirs.<session>per source, and an emptyresolvedslot to write into. - The merged result is built in a staging session named
<target>.mergingbefore it lands. Every file written there is read back and checked against what the plan said it would be. - The gate is on by default whenever a resolution was authored or a driver wrote conflict markers, so resolved work never lands untested. Start the staging session, build and test, then
turbo merge --continue. Use--no-gateto opt out and--gateto force it for a clean merge. - Editing anything in the staging session between the merge and
--continueis recorded as a post-merge edit, whether or not the path was in conflict. - Registry values and files are merged by default, and lock files, crash dumps and the per-machine local layer are left out entirely.
- Application state is merged at a second level. A
turbo statedescriptor claims a region of the session's overlay as a state unit with a type, a class and a structural signature. A claimed region leaves path-by-path classification and is merged once, at its format's own grain, under the policy its class sets. What counts as application state comes from those descriptors and from nothing else, so the merge learns every application the state subsystem learns. - A unit whose signature differs between two sessions is at a different state schema version on each side. Their values are not comparable, so the merge reads no content and reports a
versionconflict. - Sessions forked onto a different image set with
fork --imagesare refused, because their overlays cannot be compared path for path. - Hardened sessions created with
--sandboxare refused: their state lives in service-owned storage the client cannot read.
Merge Drivers
The driver for a path is chosen by its name and content type.
| Driver | Applies to | Behavior | Conflict markers |
|---|---|---|---|
text | Source, configuration and other text files | Line-based three-way merge; hunks that do not overlap combine | Git-style <<<<<<< blocks written into the file |
keyed | .json, .ini and .plist files and registry values no descriptor claims | The same key-level merge a claimed unit gets, without a class or a signature above it; arrays are compared whole | Written beside the file, so the document stays parseable |
binary | Anything else | Identical content is kept; anything else needs a side chosen | Written beside the file |
unit | Every path a turbo state descriptor claims | Three-way at the unit's own grain, listed under Unit Types below. The unit's class sets the policy below that | Written beside the file |
git-repo | A repository's .git directory | The working tree merges normally; the repository is kept from the target, so it sees the merged tree as uncommitted changes | Never conflicts |
ignore | Lock files, crash dumps, the local layer | Left out of the merge | Not applicable |
Unit Types
The unit driver merges each unit type at the grain that type's own editor addresses. The last column says whether a merged file comes back with the comments and the ordering the user wrote.
| Type | What one entry is | Comments and order preserved |
|---|---|---|
json | One JSON Pointer to a leaf. An array is one entry, because an element's position is part of its meaning | Key order preserved; the format has no comments |
jsonc | Same as json | Yes, both |
ini | A section and a key together, so the same key under two headings is two entries | Yes, both |
rdp | One connection setting. The setting's type is part of its value, so a setting that changed type is a changed setting | Yes, both |
registry | One value, compared as <kind>:<canonical bytes> so two values differing only in type are different values | Not applicable |
dir | One file of the tree, by content hash | Not applicable |
kv | One entry of the store, by content hash | Not applicable |
sqlite | One row, addressed as described below | The result is the target's own database with rows applied, so everything else is byte-identical |
plist | One key path to a leaf. An array is one entry. Binary property lists come back binary | Key order preserved; the editor drops XML comments |
xml | One element's text, or one attribute. An element's position among its same-named siblings is part of its address | Order preserved; the editor drops comments |
yaml | One mapping path to a leaf. A sequence is one entry | Order preserved; the editor drops comments |
properties | One property key | Yes, both |
mozprefs | One preference. Its type is part of its value, so a boolean true is not the string "true" | Yes, both |
A sqlite unit is addressed row by row. A row's identity is the table's primary key where one is declared, and otherwise a single column carrying a unique index, which is the shape a key-value table such as VS Code's ItemTable takes. A row's value is the remaining columns, each in a canonical form that keeps its storage class, so a text 1 and an integer 1 are different values. Where a sub-unit selects rows by a key pattern, the merger sees only the rows that pattern matches; a table with neither a primary key nor a unique column is irreducible, naming the table. The merged result is produced by applying inserts, updates and deletes to a copy of the target's database, never by rewriting the file, so indexes, user_version and every page the merge did not touch survive unchanged.
State Unit Classes
The class a descriptor gives a unit is the merge policy for that unit.
| Class | Meaning | Merge policy |
|---|---|---|
critical | Must survive; often licensing, identity or secrets | Merged at the format's grain. Every collision is a conflict for whoever is resolving; --prefer never settles one |
important | The user's own configuration and data | Merged at the format's grain; disjoint keys, entries and values combine without a decision |
derived | Reconstructible from other state, such as recent-file lists and window layout | The target's copy is kept, with one line saying so |
cache | Disposable | Left out of the merge entirely |
A unit its descriptor marks as secret-bearing, such as a browser cookie store or a credential cache, is never read. Its conflict directory in the workspace holds the signatures alone, choosing a side is the only resolution it accepts, and --units=none does not reopen it.
A unit whose parts are classified differently, such as a key-value store holding both identity and recent-file lists, splits into sub-units along the rules its descriptor gives. A part the split cannot place is treated as critical.
A unit type the merge does not yet understand is still classified and still follows its class policy. A collision inside one is irreducible, with the reason.
Conflict Classes
| Class | Cause | How to resolve |
|---|---|---|
value | The same path changed differently on two or more sides | The driver merges it where it can; otherwise --accept, --resolve or --prefer |
version | A state unit is signed differently on two sides, so they are at different state schema versions | Migrate the older side with turbo state plan and turbo state run, then merge again, or choose a side with --accept. Never merged automatically; --prefer does not apply and authored content is refused |
structural | Deleted on one side and modified on another, or a file on one side and a directory on another | --accept or --resolve only; --prefer does not apply |
dependent | A rule binds paths that must move together, such as a registry key with its subkeys or a .git directory | Resolved as one group, by the group's id |
irreducible | Content no driver understands changed on more than one side | A side must be chosen |
policy | Policy forbids writing the path into the target | Drop the path or escalate |
Options
Planning
--preview— Compute the plan, print it, and write nothing.--base=VALUE— Use this snapshot as the merge base instead of the computed one. Needed for sessions forked before fork points were recorded. A snapshot id issnp-and sixty-four hexadecimal digits, so any leading part of one is accepted as long as it is at least eight digits and names exactly one stored snapshot; a prefix that names several is refused and lists them.--path=VALUE— Restrict the merge to a subtree. Repeatable.--registry-path=VALUE— Restrict the merge to a registry key. Repeatable.--subsystems=VALUE— Which subsystems to merge. Supported values:files,registry. Default: both.registryis refused on macOS, which has no registry.--units=VALUE— Which state units to merge semantically. Supported values:all(default),none, or a comma-separated list of unit-id globs such asvscode.*.nonemerges every claimed path as a raw file instead, which is the escape hatch for a descriptor that is wrong about a path; it does not open a secret-bearing unit.
Resolution
--prefer=VALUE— Default side for value conflicts. Supported values:ours,theirs, or a session name.theirsis only valid with one source. Does not apply tostructural,versionorpolicyconflicts, nor to any conflict inside acriticalstate unit.--accept=VALUE— Resolve one conflict by taking a side, as<id>=<session>or<id>=ours. The id is a conflict id or, for a single-path conflict, the path itself. Repeatable.--resolve=VALUE— Resolve one conflict with content you wrote, as<id>=<file>or<id>=delete.<id>alone takes whatever is in the workspace'sresolvedslot. Repeatable.--resolutions=VALUE— Apply a JSON array of resolutions from a file. This is the form an agent uses.--reason=VALUE— Rationale recorded with this invocation's resolutions, in the session record and the audit stream.--markers— Put both sides into the result and carry on instead of stopping. Text files get conflict markers; other formats get each side written beside the file.
Landing
--new=VALUE— Write the result to a new stopped session with this name and leave every input untouched.--gate— Stop after building the result so it can be started, tested and fixed before it lands.--no-gate— Swap without stopping, even when resolutions were authored.--verify=VALUE— Run this command in the staging session; swap only when it exits zero.
Resuming
--status— Print the pending merge for the target: its conflicts, what is resolved and by whom, and where it stopped. Add--format=briefto print it as a prompt for whoever has to finish it.--continue— Finish a pending merge. Refuses while any conflict marker remains.--abort— Discard a pending merge and leave the target as it was. After a completed merge it restores the overlay the merge replaced, which is the snapshot recorded asmergeUndoSnapshot.
Output
--format=VALUE— Use the specified format for output. Supported values:json,brief.
Examples
Branch a prepared session twice and see what the branches did:
bash
turbo fork dev-vscode agent-a
turbo fork dev-vscode agent-b
turbo merge --preview dev-vscode agent-a agent-btext
Merge base snp-4WCZvMDCY7JJSFEvh
union 142 paths
identical 3 paths
kept target 61 paths (derived units)
excluded 88 paths (cache units)
units vscode.settings (jsonc, 1 path), vscode.extensionsDir (dir, 2 paths), vscode.globalStorage.state (sqlite, 1 path), vscode.globalStorage.state#mru (sqlite, derived, the target's copy kept (1 path)), vscode.globalStorage.state#secrets (sqlite, credentials, the target's copy kept unread), vscode.workspaceStorage (dir, derived, the target's copy kept (61 paths))
conflicts 3
cfl-4MJyMitrdrh8nThz8 value src/server/routes.ts agent-a overlaps at base line 31-44
cfl-7Qk2mVxN1sRtYbZwP structural docs/old-readme.md deleted by agent-a, modified by agent-b
cfl-9Fs5pLwR3mQtXvNdC value state.vscdb vscode.globalStorage.state (sqlite, critical): entries ItemTable/workbench.panel.stateThe editor's key-value database is one unit, and its parts are not merged alike. The recent-workspace rows are derived, so the target's copies are kept and nobody is asked about them. The credential rows are secret-bearing, so no side's content is read at all. The rows that are neither combine row by row, and the one row both sides wrote differently is the conflict above, named by its table and key.
Resolve from outside, submitting decisions with reasons and proving the result before it lands:
bash
turbo merge dev-vscode agent-a agent-b --resolutions=decisions.json --verify="npm test"decisions.json holds one object per conflict:
json
[
{
"id": "cfl-7Qk2mVxN1sRtYbZwP",
"action": "accept",
"session": "agent-b",
"reason": "agent-a's delete predates agent-b's doc fix"
},
{
"id": "cfl-4MJyMitrdrh8nThz8",
"action": "resolve",
"content": "answers/routes.ts",
"reason": "kept both route registrations, ordered by path"
}
]Resolve from inside instead, using the same idiom a coding agent already uses for a git conflict:
bash
turbo merge dev-vscode agent-a agent-b --accept=cfl-7Qk2mVxN1sRtYbZwP=agent-b --markers --gate
turbo merge --status dev-vscode --format=brief > brief.md
turbo start dev-vscode.merging
# resolve the markers and run the tests in that session, then:
turbo merge --continue dev-vscodeUndo a merge that has already landed:
bash
turbo merge --abort dev-vscodeA merge whose sources still write to host paths has nothing to combine. Under the default write-copy isolation a write to a host path goes to the host, so two branches editing a host-mounted workspace overwrite each other. Run the session with merge or full isolation for the workspace before forking it.
Output
With --format=json the command emits a merge object holding the plan, the conflicts and the outcome:
json
{
"merge": {
"target": "dev-vscode",
"sources": ["agent-a", "agent-b"],
"base": "snp-4WCZvMDCY7JJSFEvh",
"union": 142,
"identical": 3,
"keptTarget": 61,
"units": [
{
"id": "vscode.settings",
"type": "jsonc",
"class": "critical",
"outcome": "merged",
"paths": 1,
"detail": "1 path"
},
{
"id": "vscode.globalStorage.state",
"type": "sqlite",
"class": "critical",
"outcome": "merged",
"paths": 1,
"detail": "1 path"
},
{
"id": "vscode.globalStorage.state",
"subUnit": "mru",
"type": "sqlite",
"class": "derived",
"outcome": "kept-target",
"paths": 1,
"detail": "derived, the target's copy kept (1 path)"
},
{
"id": "vscode.globalStorage.state",
"subUnit": "secrets",
"type": "sqlite",
"class": "critical",
"outcome": "kept-target",
"paths": 1,
"detail": "credentials, the target's copy kept unread",
"secretBearing": true
}
],
"conflicts": [
{
"id": "cfl-4MJyMitrdrh8nThz8",
"class": "value",
"subsystem": "files",
"paths": ["src/server/routes.ts"],
"driver": "text",
"detail": "agent-a overlaps at base line 31-44",
"resolved": false,
"sides": [
{ "session": "dev-vscode", "change": "modified", "entry": "file", "hash": "…", "size": 4096 },
{ "session": "agent-a", "change": "modified", "entry": "file", "hash": "…", "size": 4210 }
]
},
{
"id": "cfl-2NpQrStUvWxYzAbCd",
"class": "version",
"subsystem": "files",
"paths": ["@PROFILE@\\AppData\\Roaming\\Code\\User\\settings.json"],
"driver": "unit",
"unitId": "vscode.settings",
"unitType": "jsonc",
"unitClass": "critical",
"detail": "vscode.settings is at different state schema versions: agent-a jsonc:5f3a…, dev-vscode jsonc:91cd…",
"resolved": false,
"signatures": {
"dev-vscode": "jsonc:91cd…",
"agent-a": "jsonc:5f3a…"
},
"sides": [
{ "session": "dev-vscode", "change": "modified", "entry": "file", "hash": "…", "size": 812 },
{ "session": "agent-a", "change": "modified", "entry": "file", "hash": "…", "size": 907 }
]
}
],
"outcome": "conflicts",
"markersWritten": false
}
}outcome is one of preview, conflicts, gate, merged or aborted. Each entry in units reports one state unit, with outcome one of merged, kept-target, excluded, conflict or unchanged. A unit whose descriptor classifies parts of it differently reports one entry for the whole unit and one more per part, each with its own subUnit and class. A conflict inside a unit carries unitId, unitType and unitClass, plus subUnitId when the conflict is confined to one part, and a version conflict also carries each side's signatures.
Registry Value Encoding
A registry value is compared, reported and answered in as <kind>:<canonical bytes>. The kind leads, so two values that differ only in type are different values.
| Kind | Canonical bytes |
|---|---|
String | The characters, as written |
ExpandString | The characters, unexpanded |
MultiString | The strings joined by a newline, in order, with no trailing separator |
DWord | The decimal integer, with a leading minus for negatives |
QWord | The decimal integer, with a leading minus for negatives |
Binary | Uppercase hex, one byte per pair, separated by - |
On macOS
The verb, its options, its output and its exit codes are the same on macOS. What differs is who owns the storage underneath it.
- Every session in the merge must be stopped, sources as well as the target. The Turbo sandbox service owns container storage on macOS and cannot pause a running session while its overlay is read, so a running source is refused by name before anything is frozen, and nothing is left behind. On Windows a running source is suspended for the moment it takes to freeze it and resumed afterwards.
- The staging session the result is built in is an ordinary container the sandbox service creates, named
<target>.merginglike everywhere else. It appears inturbo sessionswhile the merge waits at the gate, and--verifyruns its command there through the ordinaryturbo runpath. - Freezing a session, branching one, building the merged result and putting it in place are all operations the sandbox service performs. A service that does not provide them refuses the merge and names what is missing, like this:
text
This Turbo sandbox service cannot branch sessions: it does not provide snapshotContainer.
Forking and merging a session need the container snapshot operations snapshotContainer,
forkContainer, materializeMerge and replaceContainerSandbox, which arrived in Turbo sandbox
service 26.9.11.1. Update the Turbo sandbox service to 26.9.11.1 or later, then run the
command again. Nothing has been changed.- There is no registry on macOS.
--subsystems=registryis refused by name rather than accepted and matching nothing, and--registry-pathmatches nothing. - The sandbox service must be version 26.9.11.1 or later, which is where the four container snapshot operations arrived. The refusal above names that version.
- An editor's profile is not in the container root on macOS. It lives in an encrypted image the sandbox service attaches when the session starts, and the service freezes its contents into the snapshot under a
profile/prefix, so what a state unit callsCode/Userisprofile/Userthere. The merge is unaffected; it reads the unit, not the place.
A snapshot has the same identity on both platforms. A session's overlay is named by what it holds, so the same overlay frozen on Windows and on macOS is the same snapshot with the same snp- id.
Exit Codes
| Code | Meaning |
|---|---|
0 | Merged, or a preview that found no conflicts |
20 | Conflicts are unresolved and the merge is pending. Nothing in the target changed |
21 | The result was built and is waiting at the gate, or a --verify command failed |
-1 | The merge could not be attempted: no common base, a running target, or a session it cannot read |
Codes 20 and 21 are outcomes rather than errors, so a script or an agent can tell a decision it has to make from something that went wrong.
Related Commands
- turbo fork — branch a session and record the fork point that
mergeworks over. - turbo diff — see what one session changed against its base images.
- turbo commit — turn a session's changes into an image.
