Skip to content

D311 — The server settles reserved-file conflicts itself, and a write survives a failed fetch on a current base

Decision. data/log.md gets merge=union in a committed .gitattributes (scaffolded by Init, added to older KBs in a commit of its own after their first successful sync). A rebase conflict whose files are all reserved (log.md, index.md, _map.md, _archive.md) is settled by the local-profile pull with one merge commit of the remote SHA: union for log.md, the remote's version for the structural files. Alongside a concept, a reserved file stays out of the registry and is resolved the same way at finalization; a reserved-only conflict the server could not settle is registered with kind: reserved and resolved through git_conflict_resolve (union|ours|theirs). Separately, a write whose fetch fails proceeds when HEAD has no commit missing from origin/<branch>: it commits locally, defers the push and reports degraded. No step and no message asks an agent to run git in the clone.

Why. GitPathToConceptID maps no reserved file, so a conflict on log.md from two clones appending at once registered nothing: the write said "0 concept(s)", conflicts_list was empty, git_conflict_resolve had nothing to act on, and the KB stayed frozen until someone edited .git/ by hand. log.md is append-only, so union is its correct merge; index.md is regenerated by every write and the other structural files are scaffolding whose canonical copy is the remote's. For the network: D237 failed every write whose fetch failed, so a 15s blip lost the write even when the local base had nothing to refresh against. With nothing unpushed the base is as current as the last fetch left it; the cost is a fork when a colleague pushed in between, which the next successful pull rebases like any concurrent write. Taking the remote for structural files costs a curated _map.md edit made concurrently on both sides: the local one is dropped.

Alternatives rejected. - Commit the resolution on top of the local history and retry the pull, as the plan first proposed: the rebase replays the local commits one by one, and the commit that touched the file meets the remote's version again — the same conflict. A merge commit, as FinalizeConflicts already makes, is the only shape a fast-forward push accepts. - Resolve inside the live rebase (rebase --continue after staging): the abort flow in gitx.PullRebaseAutostash would have to be split around a resumable rebase, and a failure halfway leaves the state D155 had to defend against. - theirs for a log.md the attributes did not cover (a KB still without .gitattributes): it drops the local entries, the opposite of what the union attribute is for. - merge=union for every reserved file: two concurrent edits of _map.md would interleave lines into a contract nobody wrote. - .git/info/attributes instead of a committed file: it does not travel to other clones, and the conflict is between clones. - Keep failing writes on a failed fetch (D237 as written): the honest answer only while the base could have been refreshed; with nothing unpushed the refresh could not have changed what the write commits on. - Allow the write on a failed fetch even with unpushed commits: those commits may already be stale against the remote, and stacking more on them widens the eventual rebase.

Consequences. The auto-resolution runs in the local profile only (server profile rebases keep RecordServerRebaseConflict). An expanded concept's index.md is treated as a reserved file and takes the remote's version (plan #513 may later map it to its concept). The merge commit left by an auto-resolution must be pushed before the remote advances again, or the next pull --rebase linearises it and replays the original conflict — the same exposure FinalizeConflicts has. A read-side sync can add the .gitattributes commit, which then waits for the next push. MarkPushPending keeps a degraded status like a failed one, so a debounced write on a local base reports degraded.