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.