This is the ordinary agent-first case:
Branch A adds
Users.timezone.Branch B adds
Catalog.search_text.Both started from snapshot
S.A merges and its migration runs while B is still in progress.
B later incorporates A and merges.
Before either PR
repository target: S
production frontier: {S}
S
├── Users(id, email)
└── Catalog(id, title)
Each branch runs ship db diff against the same Start snapshot:
c73d54e1 (A: add Users.timezone)
/
56d441f7 -+
\
483d20eb (B: add Catalog.search_text)
A and B are complete Start -> End edges. Neither timestamp says that one runs after the other.
A merges and runs
Main contains S -> A, and production follows that path:
repository target: A
production frontier: {A}
S --add Users.timezone--> A
The migration and frontier update commit together. B's repository still contains S -> B; nothing rewrites or applies it while B is unmerged.
B incorporates current main
After Git combines A's migration directory, B's migration directory, and the declaration containing both columns, the checkout contains:
A
/
S --------+
\
B
desired declaration: M = Users.timezone + Catalog.search_text
production frontier: {A}
The two leaves are already deployable. ship db status reports a compatible two-head declaration, and ship db apply can apply whichever parent migration is missing. Until a checkpoint exists the live state is the frontier {A, B}, rather than a fictional single schema marker.
Before authoring the next migration, ship db diff gives that frontier one canonical checkpoint:
ship db diff AppDb --name converge src/Schema.shp
Ship first proves that each branch's typed operations remain valid after the other branch. It then writes one multi-parent checkpoint:
A
/ \
S --------+ +-- U
\ /
B
The checked-in source records ancestry and the canonical combined snapshot:
unify AppDb(
parents = [
snapshot("483d20ebf564f6f9"),
snapshot("c73d54e1a6340ac7"),
],
to = snapshot("adee3c9370044435"),
) {}
A unify checkpoint has no SQL file and no operations. It says that all parent histories are prerequisites and that their compatible union is U:
snapshots/adee3c9370044435.json
shp/20260915072300_converge.shp
no convergence SQL file
What production runs
Production's ledger already contains A. To reach U, ship db apply walks U's ancestors, skips A, applies B's original checked-in migration, and then records the unify checkpoint:
ALTER TABLE catalog ADD COLUMN search_text TEXT;
The database frontier changes from {A} to {A, B} when B commits, then from {A, B} to {U} when the checkpoint commits:
repository target: U
production frontier: {U}
Applied migration nodes: S, A, B, U
The same target works from every live position:
at S, apply A, B, then U;
at A, apply B, then U;
at B, apply A, then U;
at
{A, B}, record only U.Ordinary migrations keep one
fromsnapshot. Onlyunifyhasparents, and every parent is a prerequisite rather than an alternative starting state. The full applied ledger lets Ship identify exactly which ancestors are still missing.