Skip to main content
A push refuses to overwrite a remote that is absent from the local history. Refusing is safe and incomplete: it leaves the resolution to hand-editing, and it means a partial install can never be published back over the tag it came from. Reconciliation is what that refusal was standing in for. The thing that makes it different from git is what is being merged. Git merges lines of text. Here the unit is an immutable, content-addressed block. A textual conflict is not representable, and everything below follows from that, so it is worth establishing first.

No block is ever modified

You cannot edit a block, and neither can anyone else — not yours, not one you pulled from someone else’s brain. This is not a permission the protocol withholds; there is no operation that could express it. Four things stand in the way, and each one is enough on its own:
  1. The object is frozen. Block models are immutable — block.statement = "something else" raises.
  2. block_id is the hash of the envelope. Write the “same” block with a different statement and you get sha256:c261… where you had sha256:ad3d…. That is not the block modified; it is a different block.
  3. The store cannot file bytes under a foreign identity. put_bytes returns the hash of what you handed it. There is no way to say store these bytes as that identity.
  4. Tampering is caught on read. Corrupt a blob out of band and resolving it raises stored bytes for block_id sha256:ad3d… hash to sha256:c261…: the store is corrupt. A consumer recomputes the hash, so a publisher cannot serve different bytes under a known identity either.
So the question “what if two people edit the same block?” has no answer here, because the situation cannot arise. What people actually do is one of three things, and each is a case reconciliation handles: None of them touches the original. That is why reconciling a module is set arithmetic over identifiers, and why it converges whichever side you call yours.
One property that surprises people coming from git: if two people make the same correction independently, they produce the same bytes, therefore the same block_id, therefore nothing to reconcile. Identical work converges for free rather than conflicting.
There is exactly one operation that touches bytes already written, and it does not modify them — it destroys them. redact tombstones a block’s bytes for law or safety, never for correcting knowledge. It is refused by the default policy and by PERMISSIVE_POLICY; a deployment must name the redactable media types explicitly. Afterwards the block is still a member, the composition still verifies, and resolving it raises BlockTombstonedError — reported as tombstoned rather than missing, so a removed block is never indistinguishable from a corrupted one. See Retention.

The whole structural change

A field went from scalar to list. No new document. A linear history carries one entry, a root snapshot carries none, a reconciliation carries two or more. Order is significant in exactly one way: the first parent is the history the reconciliation was performed onto, and every rule that speaks of the parent means that one. The rest are merged-in history and grant nothing.
On the wire, one parent is written as the scalar parent and two or more as the list parents. So a linear history keeps the exact bytes — and the exact digest — it had before parents existed, and a client that knows nothing of reconciliation stops being able to read a brain only at the point where that brain genuinely reconciled something. That is the same oldest-that-fits rule block schemas follow.

The four steps

Step 3 either lands a new version or raises ReconciliationHaltedError without writing anything — see Resolving what did not apply.

The plan is the review

Reviewing a pull request means reading a diff. Here the incoming blocks are candidates, the ingestion gate applies unchanged, and every incoming block arrives with a verdict — so which parts of a contribution fit is known before anything is decided. plan_reconcile does not ask which strategy you picked. The composition is identical under all three, so its job is to inform that choice.

The ancestor cannot be skipped

Without it, a block present in one composition and absent from the other is ambiguous between they added it and I dropped it, and those demand opposite outcomes. Two histories that share no ancestor raise NoCommonAncestorError rather than being merged on a guess.

You choose: merge, rebase or squash

All three produce the same set of blocks. There is nothing to replay sequentially, because a snapshot is a complete statement of composition rather than a patch. What differs is only the lineage recorded — and therefore who remains on record as the author.
In git, choosing among these is a question of tidiness. Here it is a question of attribution: rebase and squash mint new snapshot identities, so a contributor’s signature no longer covers anything in the resulting history and their work ends up signed by whoever performed the operation. That may be exactly what you want for a small reviewed contribution. It is not something the SDK will decide for you:
ReconcileRequest.strategy is required. There is no default and no policy-level default — you choose per operation, the way you write git merge or git rebase.
This SDK implements no signing yet, so their_signatures_survive describes what will hold. It is reported now because the decision is made now: a report that appeared only once authenticity shipped would arrive after the only moment it could have informed anything.

Rebase preserves granularity only where it can

A published artifact carries the compositions of one version, its head. A history’s intermediate versions arrive as snapshot documents whose composition documents never travelled, and a Merkle root commits to a set without being invertible into it. So a rebase over a fetched contribution replays what it can and says how much granularity was available — rather than quietly behaving like a squash.

Exclusion wins, and that is a feature

Everything either side added is kept; everything either side removed stays removed. A block one side dropped does not return because the other side still held it. Dropped evidence also cannot be smuggled back by re-ingesting it: re-registering the same source yields the same identifier, so the reconciliation recognizes it as something this brain removed rather than as a new contribution. No special rule required.
A module that is absent is not a module that was emptied. A selective install does not hold every module, and only blocks missing from a composition both sides hold count as removals.

The conflicts that are real

Because no textual conflict is representable, what remains is semantic — and the case that proves reconciliation cannot be purely structural is this one:
Branch A drops canonical block C. Branch B adds a semantic block derived from C.
Each module’s set arithmetic is individually correct: the drop is respected in the canonical composition, the addition in the semantic one. The result still violates R1, because a derived block now cites evidence that is not in the composition. The invariant that broke lives between modules, and a set operation never crosses that boundary. So the structural result is validated as if it were an ingestion:
A conflict in this protocol is a validation failure, not a differencing failure.
Every question these conflicts raise — does this evidence exist, does this contradict what is held, is this relation admissible, does the payload satisfy a registered schema — is already asked on the ingestion path. No new checks are introduced.
Only blocks that emerge VALIDATED enter on their own. If anything did not apply cleanly, nothing is written — the reconciliation stops and waits for you. The same holds if it would take work out of your brain; see removals.

Every case, and what you do about it

Automatic unless the last column says otherwise. Nothing here is a merge conflict in the git sense — there is no half-merged state on disk and nothing to hand-edit, because the unit is an immutable block and the only questions are whether it enters and, where two histories disagree, which one prevails.
Only the rows that touch what you already hold, or that the protocol declines to decide, stop the operation. A contribution whose every block applies commits in one call.

Resolving what did not apply

Committing the part that fits would be a decision about the rest, taken without asking — the contributor loses those blocks and nobody was consulted. So it halts, the way a merge conflict does. The state lives in a reconcile pointer next to head, so it survives the process: a decision taken today is still there tomorrow, and a separate tool can read what is open. While one is unresolved, ordinary writes are refused — the decisions were taken against a particular head, and a commit underneath them would leave them describing a reconciliation that no longer exists.
The plan is recomputed on every call, not remembered. It is a deterministic function of your head, the other history, the ancestor and the blocks in the store, so recomputing it costs little and cannot report a judgment that has since stopped holding. What gets persisted is what a person decided.

The three decisions, and the one that is not on offer

Admitting a contradiction is legitimate: a contradiction is information, not a defect, and holding two claims that disagree is a state the protocol permits without deciding between them. Admitting a rejection is not, and this is the one place the model departs from version control on purpose. In git you can force anything into a commit. Here a derived block whose evidence the composition does not hold cannot be audited against its source, verify() would not catch it — it recomputes hashes and compositions, not citations across modules — and so the option does not exist:
There is nothing to hand-edit either. The unit is an immutable block, so there is no third version to write between two — the only questions are whether it enters and, where two histories disagree, which one wins.

Settling precedence

Both histories’ supersession edges stay recorded — the record of what each side did is not what gets resolved. What the decision adds is one more edge, from the winner over the loser, which is the only way this architecture states precedence. Afterwards the question is closed and only the winner is accessible:

When the reconciliation removes your work

Exclusion has precedence in Equation 1, so a block the other history dropped leaves your composition too. That is deliberate — it is what stops a drop from being undone by whoever still held the block — and it is also a decision about your work, so it does not happen without being seen:
One answer rather than one per block, because the granularity would be false: there is no per-block choice to offer when exclusion wins by construction. What is genuinely open is whether this reconciliation happens at all, and the alternative to accepting is reconcile_abort(). Re-admitting a removed block afterwards remains possible and remains an ordinary commit — doing it inside a reconciliation would make the arithmetic depend on who was resolving it. Nothing is destroyed. The bytes stay in the store, older retained roots still name the block and still verify, and the removal is auditable without a record written here: the history that dropped it wrote one, and Equation 1 keeps it like any other provenance block.
The acceptance records what it covered. If the plan is recomputed and something else would now leave — you fetched the rest of a partial history, say — the earlier statement no longer answers the question and removals_accepted goes back to False.

Withdrawn evidence takes what cites it

Equation 1 is applied per module and is individually correct in each, which is exactly why it is not enough. If the other history withdrew a canonical source and you hold a block derived from it, the source leaves and the dependent would stay behind, citing evidence the composition no longer holds. R1 is violated and verify() would not catch it — it recomputes hashes and compositions, not citations across modules. So the reconciliation runs the same cascade a drop runs, and the dependent follows its evidence out. The validation gate catches the mirror case, where the derived block is the incoming one; it cannot catch this one, because nobody proposed the block — it was already here. The consequence gets its own removal record, attributed to whoever accepted the removals: the other history recorded withdrawing the evidence, and nothing yet recorded what that cost here. The record lands in the same version as the exclusion it explains. That matters most for a rebase, which writes one version per version it replays: the step that withdrew the evidence is the step the dependent leaves on, and the steps before it keep both. Applying the whole contribution’s cascade to every step would publish versions excluding a block whose evidence they still hold, with nothing on record saying why — an unexplained removal, which is the one thing an auditable history cannot contain.

Abandoning

reconcile_abort() discards the decisions. Nothing is undone, because nothing was written: a halted reconciliation never touched a composition or the head pointer. The blocks it fetched stay in the store, unreachable from any root, for a prune to reclaim — the ordinary fate of anything no version names.

Missing evidence has two causes, and the diagnosis matters

The block is rejected either way. Why the evidence is absent determines what the contributor should be told, and provenance already holds the answer, because it is the removal ledger.
Same verdict, opposite advice. Collapsing the two discards legitimate work over a packaging mistake. Rejection is not final: if a replacement source is later registered, rederive can rebuild what was lost against it. That is a separate, deliberate step, and it is never folded into a reconciliation — one that quietly re-derived rejected blocks against substituted evidence would be inventing knowledge in the middle of an operation you believe to be mechanical.

A contribution needs no new object

Public read, restricted write. A contribution is a snapshot in another repository whose parent belongs to the maintainer’s history — the parent pointer is the entire link, exactly as a branch is in version control. There is no proposal object and no pending state, and this protocol defines no notification mechanism, in the same way version control defines no pull requests.
The transfer is only the delta: the contributor’s brain shares every block it did not change with the maintainer’s, byte for byte. A contribution of forty blocks transfers forty blocks, not a brain.

Publishing back from a partial install

A snapshot produced by a partial install names roots only for the modules that install holds, so publishing it over the tag it came from would drop the rest. Reconciliation resolves this rather than working around it:
The modules this brain does not hold take their roots from the remote unchanged — a root is a complete statement of a version, so adopting one does not require holding what it commits to:
The published snapshot then names every module the remote named, and no module regresses. A push still refuses when the snapshot omits a module the remote tag carries — stated over the snapshot, because what makes a publish dangerous is that it names less than what is there.

What this does not do

No signing exists in the SDK yet, so two rows of the protocol’s conflict table are out of reach: histories carrying different trust roots must not be reconciled automatically — unioning two key lists grants the union of both sides’ permissions — and a propose-scoped snapshot must not be treated as a brain’s current state. Both arrive with authenticity.