Multi-Agent & Shared Memory · module 3 of 9 · Time, Scale, Multiplicity, Trust · lesson 56 of 84 · ~50 min
$
uv run python curriculum/advanced/cross-agent-write-conflicts/lab/lab.py
Cross-Agent Write Conflicts¶
In one line. The rule that stops a rumour is a cliff, not a slope — and an agent one notch above it overwrites the user's own belief just by being newer.
Where this sits¶
Stage: evolve · Level: advanced · ~50 min
You need first: Provenance and Trust
Concepts assumed: Competence · Memory Operations · Provenance
This unlocks: Memory Access Control
The problem¶
Measure before inventing a problem. Conflict candidates over the unconsolidated store:
27 candidate pairs
agent vs user 1 the relocation rumour against the address
agent vs agent 0
One cross-writer conflict in the whole corpus, and I4 already gets it right. The travel agent arrives at authority 0.3, arbitrate's first rule sees a relayed claim against a first-party one, and the address wins regardless of dates.
So the machinery works on the case that exists. The question is what it does one notch away from it.
Why this isn't RAG¶
Two documents disagreeing is not a problem a retrieval system has to solve. It returns both, ranked, and the model reconciles them in context — or does not, and the user sees two sources and decides. Nothing is destroyed.
A memory layer must pick, because the loser is retired and stops being retrievable. And the pick is made by a policy that is invisible to whoever wrote the losing claim. A silent, permanent, unattributable decision is a different object from a ranked list, and it is why deterministic-freshness insisted rules do this rather than a model.
Mechanism¶
FIRST_PARTY is a threshold at 0.5, and that is the whole finding. Rule 1 asks whether each writer is above the line, not how far:
user says "pescatarian" 2025-08-02 authority 1.0
calendar agent says "vegetarian" 2026-06-01 authority 0.9
by authority rule=recency winner=calendar-agent
by trust rule=authority winner=the user
Both are above 0.5, so rule 1 does not discriminate, and arbitration falls through to recency — where the agent wins for the only reason it could, being newer. An agent trusted for scheduling has overwritten a dietary belief the user stated themselves.
provenance-and-trust built the fix without wiring it: score the claim. A calendar agent asserting diet is out of domain, so its trust is 0.3, which is below the line, and rule 1 discriminates again. The substitution is one argument — arbitrate(a, b, claim_trust) — and it is now what @A3 runs.
flowchart LR
PU["the user says<br/><i>pescatarian</i>"] --> R1{"rule 1: is one side above<br/>FIRST_PARTY and the other below?"}
CA["a calendar agent says<br/><i>vegetarian</i>, later"] --> R1
R1 -->|"score the <b>writer</b>:<br/>both are above the line"| FL["no discrimination,<br/>fall through to recency"]
FL --> LOSE["<b>the agent wins for being newer</b><br/><i>a threshold, so high and highest<br/>are the same number</i>"]
R1 -->|"score the <b>claim</b>:<br/>diet is out of this writer's domain,<br/>so it falls below the line"| WIN["<b>the user wins on authority</b>"]
style R1 fill:#f9e79f,stroke:#b7950b,stroke-width:2px
style WIN fill:#aed6f1,stroke:#2874a6,stroke-width:2px
style LOSE fill:#f5b7b1,stroke:#c0392b
And it changes nothing here¶
@A3 store contents == @A2 -- same ids, same validity, same supersessions
The one real cross-writer pair is a 0.3 relay, and its out-of-domain discount lands exactly on the authority it already had. The mechanism is correct and this corpus cannot demonstrate it, which is worth stating rather than staging — the same call graph-stores made about a corpus with one node.
What the corpus does demonstrate is the shape of the gap: zero agent-versus-agent conflicts, because the two agents claim disjoint slots. residence from one, nothing nameable from the other. A store with two calendar integrations, or a personal and a work assistant, would have this problem constantly; this one has it never.
Run candidate generation on the unconsolidated store. After reconciliation the losers are retired and excluded from candidate generation, so the same call returns zero — and the absence looks like a fact about the corpus rather than about when you asked. That is how you measure this as "no conflicts" and ship.
Design decisions¶
Why not make FIRST_PARTY a gradient? Because the rule is a precedence, not a score: a relayed claim never beats a first-party one, at any date. Turning it into a weighted sum reintroduces exactly the tunable that deterministic-freshness removed, and the failure mode is a threshold nobody can explain when a user asks why their own statement lost.
Why score the claim rather than raise the agent's bar? Dropping the calendar agent to 0.4 would fix this pair and break the case it exists for — a calendar agent reporting a schedule change should win against a stale user statement. The problem is not that the agent is trusted too much; it is that trust was being asked a question it does not answer.
Why is agent-versus-agent zero and left alone? Because it is zero. Building arbitration for a case the corpus does not contain means the first real instance meets untested code, and the honest deliverable is the measurement plus a CrossWriter that reports agent_versus_agent so the day it stops being zero is visible.
Lab¶
You'll implement: cross_writer and decided_by.
Run:
uv run python curriculum/advanced/cross-agent-write-conflicts/lab/lab.py
Expected output: 1 cross-writer pair, agent-versus-agent False, then the two arbitrations — the 0.3 relay decided by authority either way, and the 0.9 agent decided by recency under authority and by authority under trust, with the winner changing.
Stretch: raise the travel agent's authority from 0.3 to 0.6 and re-run the corpus. The relocation rumour crosses the line, wins on recency, and the user's address is retired — one constant in a fixture, and the store now believes a colleague's speculation. Every defence in this module is one number away from not being one.
What this adds to the capstone¶
memlab.agents.conflicts — CrossWriter, cross_writer, decided_by, above_the_line. arbitrate, decide, decide_all and reconcile gain an optional trust argument, defaulting to raw authority so every earlier snapshot is unmoved; Pipeline.trust switches claim_trust on at A3.
Failure modes¶
| Symptom | Cause | How to detect | Mitigation |
|---|---|---|---|
| Agent overwrites a user's own belief | Threshold rule stops discriminating above the line | Arbitrate a 0.9 agent against a 1.0 user | Score the claim |
| "No conflicts found" | Measured after reconciliation retired them | Run candidate generation pre-consolidation | Measure the right set |
| Fix breaks the case it was for | Agent's authority lowered globally | Test a schedule claim from the calendar agent | Per-claim, not per-writer |
| Untested path on first real instance | Built for a case the corpus lacks | Count agent-vs-agent pairs | Report zero, don't simulate |
| A defence one constant from failing | Authority set in a fixture, never asserted | Raise it and re-run | Assert the threshold gap |
Check yourself¶
Both writers are above FIRST_PARTY. Which rule decided, and was it wrong?
Recency, and it was working exactly as specified — the newer claim about the same attribute wins. The error is upstream: rule 1 was asked "is this writer first-party?" when the question needed was "is this writer first-party about this?". Recency then arbitrated a pair it should never have been handed.
@A3 is byte-identical to @A2. Why ship the change?
Because the corpus contains one cross-writer conflict and it happens to be the case the old rule already handled. The substitution is a no-op here and decisive one notch away, and the alternative — waiting for a corpus that exhibits it — means the first real instance meets code nobody has run. The measurement is the honest part: it says the mechanism is unproven on this data, not that it is unnecessary.
Why measure candidates before consolidation rather than after?
Because reconciliation retires the losers, and retired memories are excluded from candidate generation. Ask afterwards and you get zero — a clean number that describes your timing rather than your corpus, and reads exactly like "we have no cross-writer conflicts."
Connections¶
Stage: evolve · Level: advanced · ~50 min
You need first: Provenance and Trust
Concepts assumed: Competence · Memory Operations · Provenance
This unlocks: Memory Access Control