# Psyche-reconciled context merge (version vectors, single active reconciler)

## Status

accepted (2026-06-03) — confirmed with user at D6 build start (the M4-PLAN §D6b
scheduled touchpoint). Builds on ADR-0003 (multi-instance), ADR-0011 (BranchStore),
ADR-0012 (Psyche trust boundary), KNOWN-HAZARDS 6.5.

## Context

Cross-node Psyche sync (M4-D6) makes the context store multi-writer across
machines. The local precedence guard (6.5: `source` + `routed_at` marker, direct
beats stale LLM) must generalize to distributed writes. The M4-PLAN D6b decision
already locked the *ordering* model: per-node **monotonic epoch counters + a
version vector** (one counter per node — the same `EpochSource` that feeds the
D3b registry lease), dominate→accept, dominated→drop, **concurrent→surface, never
silent newest-wins**; wall-clock is at most a human tiebreaker hint.

What remained open was the **conflict-surface UX**: what happens to a concurrent
pair `(vA, vB)` once detected. Options considered:

1. **Quarantine + manual CLI resolve** — local file untouched, remote version
   parked, `spt context conflicts` + `resolve --keep local|remote`. Safe but the
   mind stays forked until a human notices.
2. **Inline both versions** under explicit section headers — self-heals via the
   next commune but bloats/confuses the live mind file in the interim.
3. **Psyche auto-reconcile** — the mind's own author merges its own mind.

A second fork: sync transport. State-record transfer (NDJSON snapshots, each
node's git history local-only) is simpler, but produces **divergent per-node
histories** — rejected outright; the context DAG must be shared. Third fork:
BranchStore backing (git CLI vs gitoxide).

## Decision

**Concurrent context writes are reconciled by the endpoint's own Psyche** — a
bounded, stdin-fed, stdout-captured Psyche turn receives both versions (plus
vector + wall-clock hints) and emits the merged file. Mechanics:

- **Holding state is durable and replicated.** On detecting a concurrent pair,
  both versions persist as **tracked conflict artifacts** (deterministic
  content-hash names, in-repo, so conflict state itself syncs). The local
  working file stays untouched — the active mind is never clobbered by an
  unresolved conflict. An event seam fires (a D8 notif producer later).
- **Exactly one reconciler: the active instance's node** (the existing
  active-instance-authoritative rule — the Psyche lives where the mind is being
  used). No active instance anywhere → deterministic fallback: lowest node id
  among conflict holders. This kills reconcile storms (two nodes merging the
  same pair into textually-different results, conflicting again).
- **The merged write carries `join(vA, vB)` + a bump of the reconciler's own
  vector entry** — it strictly dominates both parents, so it propagates as a
  plain accept everywhere and clears the artifacts subnet-wide.
- **Failure is safe by construction.** The turn is bounded (timeout, 5.3) and
  validated (non-empty, marker-parseable); any failure — harness absent, turn
  error, garbage output — leaves the artifacts in place for retry at the next
  sync/activation. Both versions are never lost. The reconciled output is an
  `llm`-source write, so the 6.5 direct-write protection window still applies
  (a fresher direct write suppresses it; artifacts persist; retry re-merges
  against the new local version).
- **Dependency re-sequenced:** the bounded stdout-captured Psyche turn driver is
  **ADR-0012/D7.5a machinery pulled forward into D6b** (the relay/sanitize
  halves, D7.5b/c, stay at D7.5). The interim `Stdio::null()` spawn is unusable
  here (7.3).

**Transport: git-native bundles over Iroh — the context DAG is shared.** Peers
exchange incremental `git bundle`s over broker QUIC streams, **pull-based and
ref-scoped**: a node requests `a-<id>` for endpoints it has `synced(E,S)` and
`p-<project>` only for projects it hosts (the requester names refs — two-tier
scoping falls out of the pull model, no new registry fields). First contact
merges unrelated seed histories once (`--allow-unrelated-histories`); thereafter
one DAG, one merge commit per sync. The merge driver resolves **per file by the
vector rule** — never a git line-merge, never a conflict marker in a mind file.

**BranchStore v1 drives the system `git` binary** (the `git pull` handoff seam
already assumes git on the node), behind a trait shaped so a gitoxide swap is a
later drop-in. ADR-0011's commit-latency spike rides D6a empirically.

**Hub mode (ADR-0002 amendment) stays a deferred opt-in seam** — D6 is P2P only.
The shared-DAG choice is what keeps later hub interop natural (same driver, a
GitHub remote is just another peer).

## Consequences

- The mind self-heals: conflicts resolve in one Psyche turn instead of waiting
  on a human, while the durable-artifact holding state keeps the never-lose-data
  invariant when reconcile is unavailable.
- 6.5 generalizes cleanly: marker grows `spt:node` + `spt:vector`; the existing
  parse/render extends, not rewrites (anticipated in `spt-live/src/context.rs`).
- D6b swallows D7.5a — D7.5 shrinks to taxonomy + relay/sanitize.
- The reconciler election leans on registry liveness (D3); a partition with two
  "active" claims is bounded by the D3b epoch lease, and worst case produces a
  re-conflict of merged outputs — detected and re-reconciled, not lost.
- State-record sync and CRDT machinery are rejected for context (text snapshots,
  ≤N infrequent writers — vectors + reconcile are sufficient and cheaper).
