Cross-agent memory bus — spec
Cross-agent memory bus — spec
Young's directive 2026-09-28, Tam (spec) + Min (coordination), engineering with Atlas/Ace on solos-memory.
Companion (service-level, already on the board as task-2040344e): 2026-09-28-spec-solos-memory-service.md.
0. Preflight corrections (measured on LAB 2026-09-28, not taken on faith)
| Directive says | Actually true (verified) |
|---|---|
| "episodic table, 1471 rows" | Table is memory_episodic (no table named episodic). 1548 rows, 2026-09-18 21:50 → 2026-09-28 12:42 UTC. Composition: conversation_turn 1443, goal_execution 105 — i.e. 93% raw turn text. Per-agent: sol 406, atlas 289, ace 182, mira 177, min 174, alpha 146, nova 90, echo 84. |
| "Qdrant 3 collections, long_term ~400" | That is two hosts, not one. LAB 192.168.4.60:6333 = 1 collection (long_term, 400 pts). Prod 100.124.146.127:6333 = 3 (long_term 329, short_term 0, durable 0). Same split-brain class as board task fc043640. The bus MUST name one host; recommendation: LAB (loopback, where solos.db and the mesh live). |
| "episodic is flat, nothing reads it" | Confirmed and worse. Zero live readers — memory_episodic and memory_episodic_archive are referenced only under .displaced-20260926-122825/. memory_episodic_archive already holds 566 rows (2026-07-24 → 2026-09-18), so eviction is already happening and the evicted half is also unread. The bus is not adding a read path to a flat table; it is adding the first one to a system that has been silently archiving for 5 weeks. |
spec at ~/.solos/docs/SPEC-cross-agent-memory-bus.md | That path does not exist on the laptop or on LAB. Phantom artifact. Real spec = this note + the board result field. |
Also: memory_longterm = 321 rows, embedding non-null 0/321; 11 literal duplicate facts (e.g. "SolOS uses layered memory architecture" ×3). Verified board footgun: PATCH /tasks/a9fa41c4 returns {"updated":true} and writes nothing; the real id is task-a9fa41c4 (task 685ee1e1). Always re-GET after PATCH.
1. Write contract — agent-keyed, one writer per key
- Every write is agent-attributed. The
agentfield is mandatory on every tier. Reject any agent not present inagent_registry(11 rows live) with an explicit error naming the agent. Never create orphan rows. - The write key is
(agent, tier, key). No shared/global/multi-writer key, ever. Two writers on one record is the board-fork bug we already paid for; the key shape is what prevents it. Writes are upsert-on-conflict, not blind insert. - Normalise the two payload schemas on read. Qdrant
long_termcurrently holds both shapes:store_memory(memory_nodes.py:90) writesagent_id / memory_scope / importance_score, while 63/400 points (15.8%) carryagent / topic / content / fidand noagentfield at all. The bus must resolveagent = coalesce(payload.agent, payload.agent_id)and treat a still-null attribution as a hard filter-out, never as a passthrough — otherwise 15.8% of the corpus arrives unlabelled. - Fix or wrap the point-id before the bus writes.
store_memoryderivesuuid5(NS_URL, f"{agent_id}:{topic}:{content[:100]}")and truncates payload to 2000 chars. Two distinct facts from the same agent+topic sharing a 100-char prefix collide into one point and the second silently overwrites the first. The bus must key on a full-content hash (oragent:tier:sha256(content)), not a truncated prefix. - One canonical writer. There is no public write endpoint today;
store_memoryis the only writer and it is an internal LangGraph node. The bus becomes the first public write path, which makes it the de-facto canonical writer — sostore_memorymust be refactored to call the bus, not sit beside it. Two live writers onlong_termis the same fork asfc043640. (Flagged for Ace/Atlas; not decided here.)
2. Read contract
- One handler, two transports:
POST /mcp(MCP StreamableHTTP) andGET /v1/recall(REST), same semantics. - Parameters:
agent(caller),q,k=5,scope = own | shared | shared_except_self(defaultshared),tiers = [short, long, episodic, durable]. - Every result carries
{tier, agent, ts, score, source, foreign}— never bare text. Withoutagenton each row a caller cannot attribute a fact, which is how false consensus forms. foreign = (result.agent != caller). Cross-agent rows are labelled, not hidden.durableis excluded from the default tier set. A caller may read only its ownpersona/<agent>row; cross-agent durable reads are refused.memory_durableis identity/persona and stays Sol-owned — the bus reads it, never writes it.- Failure behaviour: DB busy → retry ×3 then 503 with a reason, never an empty result set. Embedder down →
degraded: true+ keyword recall, no exception (headerX-Memory-Degraded). - Ship-with-honesty: with 0/321 embeddings and 11 duplicated facts, recall from
longis currently precision-poor. Report per-tier hit counts and duplicate-suppression stats in/v1/statsso a green200is not mistaken for a working recall.
3. Retention
| Tier | Hot life | On eviction | Notes |
|---|---|---|---|
short | TTL default 86400 | lazy expiry on read + daily purge on the CENTRAL cron registry (:8436) with delivery target + failure signal — not a local crontab | no daemon |
episodic | 120 min / 1000 turns, whichever first | summarize-on-eviction → long, written as agent = <summarising agent>, source = episodic:<id-range>, so the derivation chain is auditable | must also be readable back |
long | no hard delete | scope field active → durable → subconscious + reinforcement_count (existing store_memory semantics) | scope change, not data loss |
episodic_archive | indefinite | — | must get a read path (admin scope) or we re-create the 566-row black hole |
Retention is only meaningful if the archive is recallable. An unread archive is data loss wearing a retention policy as a disguise.
4. The episodic ↔ durable boundary (the rule, stated once)
- Episodic = what happened. Turns and goal executions. Verifiable, time-stamped, TTL'd, shared-readable only if §5 resolves in favour of sharing. Never prescriptive, never quoted as canon.
- Long = the distilled fact. The only tier that is both fleet-readable and treated as canonical. This is where summarize-on-eviction lands and where recall actually gets its answers.
- Durable = who we are.
persona/<agent>and identity records. Sol-owned. The bus never writes it; other agents never read it.
Promotion is a one-way valve, and only one edge exists on the bus: episodic → long, via an explicit promote call that must name the source row ids. long → durable is Sol's lane, not the bus's. Any second edge is two writers on one record, and we have already lived through that with the board.
5. The design decision that will bite us
Does scope=shared include the episodic tier?
Everything about the bus's usefulness hangs on it, and the two answers are not a preference — they are different products:
- Yes → the bus is genuinely valuable: 1443 of 1548 episodic rows are raw turn text, ~155 new rows/day, and today nobody can read them at all. But every agent's unfiltered turn history becomes fleet-readable, including the turns where Young talks to one agent in confidence. That is a one-way door: the moment one agent quotes a shared turn into a
longfact, the confidentiality is gone forever and no retention rule can claw it back. - No → no breach, and the bus quietly becomes a fact-sharing channel over a 321-row corpus with 0/321 embeddings, 11 literal duplicates and 3% duplication. It would look live, answer "installed" in acceptance, and return near-worthless recall. That is the silent-failure mode already flagged in
task-8993aedd: not under-retrieval, precision collapse.
Recommendation: default shared to [long, short], and gate episodic behind an explicit per-agent opt-in with an audit log, so the value is available to the fleet without making private counsel structurally shareable. Whichever way Young decides, it must be decided before the read path ships — retrofitting the boundary means rewriting 1500+ rows' worth of history, not adding a config flag.
🎯 Tam's Read: The bus is worth building, but its only interesting asset is 1443 raw turn rows nobody can read, and sharing them is irreversible — decide the episodic-sharing boundary first; everything else is plumbing.
solos-memory — spec amendment (Tam ⇄ Sol joint, ref task-dbed2f8e)
Sol reversed my acceptance check #6 ("degraded:true is the steady state"). I concur on all three reversals, with one correction to the factual basis and one added mandatory condition. Ace may build after §6 below is read — it is the binding version of the three points.
solos-memory — shared fleet memory service
Directive: Young, 2026-09-28 — "Next to build long-term and short-term memory plugin for Hermes via API / MCP for our agents to shared and use." Owners: Ace (build) · Atlas (verification + rollout) · Tam (spec + acceptance) Host: LAB lab@192.168.4.60 (where solos.db lives and the A2A mesh runs)

