When two agents' memories conflict, tier three is an LLM

SageOx CLI

A Go CLI whose team memory is a git repository shared by humans and agents, where a pull that halts on conflicts is resolved in three tiers — git's union driver, accept-theirs confined to regenerable artifacts, then a bounded LLM whose only post-condition is that no conflict markers remain.

Carries 0 of 7 rubric mechanisms. Most systems here carry none or one (41%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

SageOx CLI is "[t]he hivemind for human-agent teams" — MIT, Go, 651,919 lines across 1,337 test files, shipping an ox binary and a Claude plugin.

Its team memory is a git repository. The cloud provisions it, the CLI clones it sparsely, and it holds MEMORY.md plus memory/daily/, memory/weekly/ and memory/monthly/ alongside sessions/ and data/. Agents write observations with ox memory put; a daemon syncs them; the server distils them into the summaries that later get read.

Two design choices are worth the visit and one is worth knowing before you trust it.

The first is how memory reaches an agent. Priming does not inject it. It hands over MEMORY.md and then a catalogue — "Recent: memory/daily/ (12 files — what happened recently)", "Patterns: memory/weekly/", "Trends: memory/monthly/" — under a heading called Progressive Disclosure, and leaves the agent to open what it wants with ordinary file tools. In a corpus where nearly every system either retrieves-and-injects or injects whole, advertising the shelf and letting the reader choose is a genuinely different answer, and it costs nothing at prompt time.

The second is the comment on DefaultResolveRules, which is the best piece of writing in this repository and possibly the best documented engineering judgement in this atlas. It explains why sessions/ auto-resolves on conflict: meta.json is written by both the cloud summarizer and the local CLI, so with no rule "the rebase halts, nothing can resolve it, the abort restores the pre-rebase state, and the next attempt fails identically — a deterministic wedge that never escalates and never self-heals." Then the evidence: "One ledger sat 341 ahead / 1055 behind for 13 days with 281 such conflicts." Then the justification, artifact by artifact, each with its canonical source named elsewhere, and the trade stated outright: "An imperfect summary beats a ledger that can never sync again." Then a SAFETY paragraph about pointer-wins ordering that ends "Do not weaken that guard", and a NOTE explaining why the rule is scoped to the ledger rather than added to the shared defaults.

Crucially, the paths that auto-resolve are data/ and sessions/ — the regenerable ones. The memory directories are not on that list. That is the right call, and it is what makes the third thing matter.

A conflict that is not under a safe prefix falls to tier three: internal/ledger/automerge runs an LLM — claude, gemini or codex, or an explicit absolute path, restricted by an allowlist with the argv[0] substitution attack spelled out in the comment — on the conflicted file, with a 60-second per-file deadline, and asks it to merge.

The system prompt is careful: "Preserve user intent on both sides; when in doubt prefer including more content over deleting. Remove all conflict markers from your output."

The verification is that the conflict markers are gone.

That is the whole post-condition. A model that resolves the conflict by keeping one side and discarding the other, or by paraphrasing both into something neither person wrote, produces a file with no conflict markers and is staged and committed. For source code that is a familiar risk with a familiar remedy — someone reviews the diff. For a team's shared memory, the thing being silently rewritten is the record of what the team decided.

2. Mental Model

An observation is what an agent records in the moment: {"content": "..."}, capped at 20KB.

A fact is what extraction produces: a headline, an optional summary and rationale, a who, a source type and reference, a timestamp, and a category from a closed list.

The ledger is a git repository shared by the team's humans and agents.

A sync is git pull --rebase, and when it halts, the three tiers decide what the team remembers.

Diagram — the accept-theirs tier is deliberately confined to regenerable artifacts, so a conflict in memory content falls through to an LLM whose only post-condition is the absence of conflict markers
Diagram source
%% caption: the accept-theirs tier is deliberately confined to regenerable artifacts, so a conflict in memory content falls through to an LLM whose only post-condition is the absence of conflict markers
flowchart TB
    A["agent"] -->|"ox memory put"| OBS[(".observations/ — local JSONL,<br/>20KB cap per observation")]
    OBS -->|"daemon sync"| LED[("team ledger — a git repo,<br/>cloned sparsely from a<br/>server-provided URL")]
    LED --> MEM["MEMORY.md<br/>memory/daily · weekly · monthly<br/>sessions/ · data/"]
    OBS -->|"POST /api/v1/teams/{id}/memory/distill"| CLOUD["SageOx API —<br/>server-side LLM distillation.<br/>The local pipeline that did this<br/>was removed 2026-09-09"]
    CLOUD --> MEM
    MEM --> PRIME["agent prime: MEMORY.md +<br/>a CATALOGUE with file counts —<br/>'Progressive Disclosure'.<br/>Content is not injected"]
    SYNC["git pull --rebase halts<br/>on conflicts"] --> T1{"tier 1 — union driver<br/>(paths declaring merge=union)"}
    T1 -->|"markers gone"| STAGE["stage"]
    T1 -->|"unresolved"| T2{"tier 2 — accept-theirs,<br/>only if ALL remaining paths are<br/>under SafePrefixes"}
    SAFE["SafePrefixes = data/ + sessions/ —<br/>regenerable artifacts, each with its<br/>canonical source named elsewhere.<br/>memory/ is NOT among them"] -.-> T2
    T2 -->|"resolved"| STAGE
    T2 -->|"a memory path conflicts"| T3{"tier 3 — LLM merge"}
    T3 --> BOUND["bounded: allowlist claude | gemini | codex<br/>or an absolute path; 60s per file"]
    BOUND --> PROMPT["'Preserve user intent on both sides;<br/>when in doubt prefer including more<br/>content over deleting'"]
    PROMPT --> CHECK{"post-condition"}
    CHECK -->|"no '<<<<<<<' remains"| STAGE
    CHECK -.->|"nothing checks that content<br/>survived — a silent drop or<br/>rewrite passes"| RISK["the record of what the<br/>team decided, rewritten"]
    STAGE --> CONT["git rebase --continue<br/>(only once ALL conflicts staged)"]

3. Architecture

Area Role
internal/ledger/ The git-backed team ledger: clone, sparse checkout, sync, push
internal/ledger/automerge/ The three conflict tiers and the LLM binary allowlist
internal/facts/ The unified Fact schema and its JSONL reader and writer
cmd/ox/memory_put.go Local observation capture
cmd/ox/memory_distill.go The call to the server's distillation endpoint
cmd/ox/agent_prime.go What an agent is told about memory at session start
internal/manifest/ Sparse-checkout paths and the resolve rules
docs/specs/team-memory-journal.md The superseded local pipeline, kept as history

4. Essential Implementation Paths

internal/ledger/ledger.go:36-71. Read the comment, not just the rule. It is forty lines of engineering judgement with its evidence attached, and it is the reason to read this project.

Then internal/ledger/automerge/automerge.go:1-20 for the tiers, and llm.go:22 for the prompt that decides a merge.

Then cmd/ox/agent_prime.go:2313-2328 for progressive disclosure.

5. Memory Data Model

The Fact is well shaped for citation: SourceRef is "the stable canonical identifier", SourceURL is "the human-clickable URL for drill-down", SourceTitle is "a short human label for citations", and the comment notes that older files lacking the last two are still valid because "the citation pipeline degrades gracefully (link omitted, label derived from other fields)". Who carries the author. A file header records a schema version, the source type, a recorded-at, an optional source hash and the query window that produced it.

What the model does not carry is any epistemic state. Category is decision | learning | open_question | action_item | context | ship | blocker | direction_change — a taxonomy of what kind of thing was said, decided at write time. Nothing marks a fact as disputed, superseded, corrected or withdrawn, and WriteFacts opens the file with os.Create, which truncates: a rewrite replaces the file rather than appending to it. The history is git's, which is real but is the repository's history rather than the memory's own record — and after an LLM merge, the git history shows a resolution commit, not what each side originally said.

That is why this report carries no marks. Every mechanism the atlas asks about is either absent or delegated: no stored status filtering a read, no validity interval, no value-keyed rejection, no scope predicate — the team boundary is a separate repository — no append-only mutation record of its own, no human review step over memory, and no committed must-not-retrieve assertion.

6. Retrieval Mechanics

There is none to speak of, deliberately. MEMORY.md is the always-present document; everything else is a file tree the agent may open. For a team whose agents already have file tools, that is a defensible answer and it makes the memory auditable by git log and readable by a human without the CLI.

The cost is that nothing ranks. An agent that does not go looking does not find the weekly summary, and the priming text is the only thing nudging it.

7. Write Mechanics

ox memory put accepts a JSON object or JSONL batch on the command line, from a file, or on stdin, caps each observation at 20,480 bytes ("~5000 tokens"), and writes into .observations for the daemon to sync. Priming instructs the agent to use it as a standing behaviour: "Proactively record observations throughout this session … Record decisions, discoveries, questions, and notable events as they happen — don't wait to be asked."

Distillation is the other half and it is not here. ox memory distill posts the pending observations and a date range to /api/v1/teams/{teamID}/memory/distill and receives a summary — the comment calls it "server-side LLM distillation". The command also refuses to run for a human, printing an explanation instead, because "[m]emory distillation is an automated process run by AI coworkers."

So the policy that decides what a team remembers — what gets summarised, what is dropped, how contradictions between two agents' observations are settled — runs on a server whose code is not in this repository. The local pipeline that once did it was removed: docs/specs/team-memory-journal.md is headed "Superseded — 2026-09-09: ox distill and its entire history command tree have been removed", and the surviving references to memory/daily/ in Go are a file count for priming and a sparse-checkout path.

8. Agent Integration

A Claude plugin plus the CLI, with agent priming, session upload and a daemon. The daemon is where the LLM resolver runs (internal/daemon/llm_resolver.go), which means the merge decision happens in the background rather than in front of whoever caused the conflict.

9. Reliability, Safety, and Trust

The LLM binary allowlist is the trust mechanism done well. claude, gemini and codex as bare names, or an explicit absolute path — "relative paths with a slash are refused outright" — with the reasoning recorded: an attacker who can write a binary earlier on PATH could otherwise "redirect to /tmp/evil", while an absolute path is trusted because "the operator already chose where to point this binary." A test file exists solely for the allowlist.

Each tier is conservative in the right direction: "a tier that can't safely resolve every remaining conflicted path returns control to the next tier", and the resolver "only calls git rebase --continue once all conflicts have been staged." The accept-theirs tier fires only when every remaining conflict is under a safe prefix, so one memory file in the set sends the whole batch to the next tier rather than resolving the rest quietly.

The gap is the post-condition on tier three, and it is worth stating precisely because the rest of the design is careful enough that it stands out. The check is strings.Contains(data, "<<<<<<<"). That confirms the file is syntactically clean. It cannot confirm that both sides' content survived, and the instruction that it should is a sentence in a system prompt. A cheap improvement exists: the tier already has both sides of the conflict, so a post-merge check that every non-marker line from either side appears in the output — or a count of lines dropped, logged and thresholded — would turn the prompt's intent into something enforced.

10. Tests, Evals, and Benchmarks

1,337 test files, including dedicated coverage for the allowlist, sparse clone, the murmur paths, github sync and the automerge tiers. The memory commands have put, write, integration and observation-roundtrip tests.

The README is candid about what is not running: a comment in the badge block reads "CI badge intentionally omitted: ci.yml is currently disabled (ci.yml.disabled). Only docs.yml and smoke-test.yml are active, and neither is a build signal." Saying so in the file where a badge would otherwise imply otherwise is the honest choice.

No test asserts anything about what the LLM merge tier preserves.

11. For Your Own Build

Copy the comment on DefaultResolveRules as a form. Name the failure, quantify it, justify the scope one item at a time, state the trade in a sentence, mark the safety constraint, and explain what you deliberately did not do. A rule documented that way survives the next refactor because its reasoning is checkable.

Confine automatic conflict resolution to artifacts you can regenerate, and name the canonical source of each one where the rule lives. data/ and sessions/ qualify; a record of what someone decided does not.

If an LLM resolves merges in a shared memory, give the tier a post-condition about content, not only about syntax. You have both sides in hand at that point; checking that neither vanished is cheap, and the absence of conflict markers is not evidence that a merge preserved anything.

Consider progressive disclosure for team-scale memory. Advertising memory/weekly/ (7 files — weekly themes) costs a line of prompt and lets the agent decide; injecting the same content costs the context window every turn whether it is relevant or not.

12. Open Questions

What the server's distillation does with contradictory observations from two agents. The request carries only observations and a date range; the policy is not in this repository.

Whether the LLM merge tier ever fires on memory paths in practice, or whether those conflicts abort. The tier ordering allows it; nothing in the tree records how often it happens.

Whether a fact can be retracted. Nothing in the schema or the CLI surface suggests a mechanism, and WriteFacts rewriting the file whole means a removal leaves no trace in the format itself.

Appendix: File Index

Path What to read it for
internal/ledger/ledger.go:36-71 Forty lines of documented judgement, with the evidence
internal/ledger/automerge/automerge.go The three tiers, and where each one stops
internal/ledger/automerge/llm.go The allowlist, its threat model, and the merge prompt
internal/facts/types.go A citation-shaped fact with no epistemic state
cmd/ox/memory_put.go The observation cap and the local-then-sync path
cmd/ox/agent_prime.go Progressive disclosure instead of injection
docs/specs/team-memory-journal.md The local pipeline that was removed, kept as history

History

2026-09-1602f4f406… — first reading, at a commit dated 15 September 2026. Screened before opening, from a shallow clone: twenty files scanned, three auto-run surfaces, one build-time execution point, one unpinned surface, five dependency files inside the seven-day cooldown, and the CLAUDE.md and AGENTS.md read as data. Nothing was installed, built or run.