solos-memory — shared fleet memory service
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)
1. Why this exists (verified baseline, 2026-09-28)
The data plane already exists in solos.db on LAB — what is missing is the service that exposes it to all 11 agents. Measured row counts:
| Table | Rows | Shape | Use |
|---|---|---|---|
memory_durable | 30 | agent, key, value, category | canonical identity/persona — read-mostly, not this service's write target |
memory_longterm | 321 | agent, topic, fact, source, confidence, embedding BLOB | durable facts + semantic recall |
memory_episodic | 1,495 | agent, event_type, summary, details, outcome, duration | turn/event timeline |
memories | 230 | agent, key, value, tags, source | legacy key/value — read-only, migrate later |
Live checks: memory_durable holds 8 persona/ rows + hermes-migration/* rows; agent_registry = 11 rows. No agent can currently read another agent's memory over any protocol — each A2A agent is an opaque peer by design (A2A spec), so shared memory must be a separate service, not smuggled through message/send.
2. Non-goals (scope discipline)
- NOT a memory provider plugin inside Hermes core (that is
plugins/memory/; we deliberately use the MCP + REST route Young asked for, so the core is untouched). - NOT a replacement for
memory_durable(persona/identity stays canonical and Sol-owned). - NOT touching
solos.dbschema in v1 — additive only, no migrations, no column changes. Read the four tables above; write only tomemory_longterm+memory_episodic(+ a newmemory_shorttable, see §5). - No vector-DB migration. v1 embeds reuse the existing
embeddingBLOB column; if no embedder is configured, recall degrades to keyword/FTS and says so.
3. Service contract
Process: one systemctl --user unit lab-memory.service on LAB, Restart=always, RestartSec=5, WorkingDirectory=/home/lab/SolOS/runtime, python venv at the existing SolOS/runtime/venv.
Bind: 127.0.0.1:8437 only. It is not added to lan-relay.py and not published over the tailnet. Reachability is via the existing loopback pattern (CostGov/registry style). Publishing is a separate Young decision (§9).
Transports — both on the same process:
POST /mcp— MCP StreamableHTTP (the reason this spec exists: Hermes discovers these natively)POST /v1/...— plain REST, same handlers, for OpenCode agents, A2A agents, shell scripts, and curl
DB access: SQLite, WAL mode, busy_timeout=5000, one connection per request (the db is 422 MB and shared with running agents — never hold a long-lived write lock). Reads may use a per-process read-only pool; writes must be short transactions.
4. API surface
MCP tools (registered as mcp_solos_memory_* in every Hermes profile)
| Tool | Args | Returns |
|---|---|---|
remember_short | agent, key, value, ttl_s=86400, tags=[] | {stored: bool, key, expires_at} |
remember_long | agent, topic, fact, source, confidence=0.5, tags=[] | {id, topic, embedded: bool} |
remember_event | agent, event_type, summary, details, outcome, duration_s | {id} |
recall | agent, query, k=5, scope="shared"|"own", tiers=["short","long","episodic"] | {results:[{tier, agent, text, score, source}], degraded: bool} |
forget | agent, key | {deleted: int} |
promote | agent, key | {promoted_to: "long", id} |
stats | — | per-tier counts + per-agent top-10 keys (diagnostic, no secrets) |
REST twin (identical semantics)
GET /health
GET /v1/recall?agent=&q=&k=5&scope=shared
POST /v1/short {"agent","key","value","ttl_s","tags"}
POST /v1/long {"agent","topic","fact","source","confidence","tags"}
POST /v1/episodic {"agent","event_type","summary","details","outcome","duration_s"}
DELETE /v1/short?agent=&key=
GET /v1/stats
All responses carry X-Memory-Degraded: true when recall fell back to keyword (no embedder) so callers never mistake degraded recall for an empty result.
5. Schema (additive only)
CREATE TABLE IF NOT EXISTS memory_short (
id INTEGER PRIMARY KEY,
agent TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
tags TEXT,
created_at REAL NOT NULL,
expires_at REAL,
UNIQUE(agent, key) -- upsert semantics
);
CREATE INDEX IF NOT EXISTS ix_memory_short_agent ON memory_short(agent, expires_at);
memory_short rows are expired lazily on read and purged by a daily timer, not by a daemon. Everything else reuses the existing tables.
6. The sharing rule (this is the whole point)
- Writes are always agent-attributed. A row belongs to the agent that wrote it. No exceptions, no shared-write key.
- Reads default to
scope=shared— any agent may recall any other agent's long-term/episodic memory.scope=ownrestricts to the caller's rows. memory_durableis NOT exposed for recall by default (identity/persona is Sol's canon). Atiersflag may include it only for the requesting agent's ownpersona/row; cross-agent durable reads return 403-equivalent. Rationale: this is the rule that prevents the board-fork bug we already lived through — two writers on one authoritative record.- Read-before-write on
recallis not required, but the service MUST returnagenton every result so a caller can attribute a fact correctly.
7. Failure behaviour (fleet rules — no silent failures)
- DB locked/busy → retry with backoff up to 3×, then HTTP 503 with an explicit reason, never an empty result set.
- Unknown agent id (not in
agent_registry) → reject the write with a clear error naming the agent; do not silently create orphan rows. - Embedder unavailable →
degraded: true, keyword recall, no exception. - Every rejected call is logged with agent + tool + reason, and a counter is exposed in
/v1/statsso a silent-failure sweep can see it.
8. Rollout (Atlas)
- Unit +
/healthup on 127.0.0.1:8437, loopback only, verified withcurl -sS http://127.0.0.1:8437/health= 200 andss -ltn | grep 8437showing127.0.0.1(not0.0.0.0). - REST proof, out-of-band:
POST /v1/longas agentatlas, thenGET /v1/recall?agent=sol&q=…and confirm theatlas-authored row comes back to a different agent. This is the acceptance test for the sharing rule. - A2A mesh untouched: ACK sweep
:8300-:8307(excluding the already-failed:8305Alpha) must stay 200 before and after. A memory service that costs the mesh a port is a failed change. - Wire Hermes profiles (
sol,min,mira, and the default root on LAB): addthenmcp_servers: solos-memory: url: "http://127.0.0.1:8437/mcp" timeout: 60 connect_timeout: 30hermes gateway restartfrom a separate shell (never from inside a gateway turn — the restart kills the command running it) and confirm themcp_solos_memory_*tools appear in the next turn's toolset. - Wire the A2A agents: each
run_.pygains a tiny HTTP client call to127.0.0.1:8437— read on turn start (top 3 shared recalls for its agent), write one episodic row on turn end. Keep it best-effort: a memory failure must never fail a turn (log + continue). - Cron: register the daily short-memory purge on the central cron registry (not a local crontab), with a delivery target and a failure signal, per Young's standing rule.
Definition of done: a fresh turn on the sol Hermes profile can call mcp_solos_memory_recall and get back a fact that mira wrote earlier that day, and the solos.db row count for memory_longterm has increased by exactly the number of writes issued.
9. Explicitly deferred / needs Young
- Publishing :8437 over the LAN relay or tailnet (same question as CostGov — governance ports were deliberately kept loopback-only).
- Any embedder/model for
memory_longterm(CostGov-owned decision; v1 ships keyword recall). - Migration of legacy
memories(230 rows) intomemory_longterm. - Cross-host memory (laptop Tam/Eva/Eve writing into LAB's db) — needs an SSH key path first; the service is loopback by design until Young says otherwise.
10. Risks
| Risk | Mitigation |
|---|---|
Service competes with agents for the shared 422 MB solos.db | WAL + short transactions + read-only pool; mesh must stay 200 throughout (Atlas step 3) |
| A memory write becomes a silent failure nobody sees | explicit 503s + /v1/stats counters + no-op-on-error in the A2A client |
| Durable/persona data leaks into shared recall | memory_durable excluded from cross-agent recall by construction (§6) |
| Another board-fork style split (two writers, one record) | agent-attributed writes only; no shared-write key; provenance returned on every row |
| Scope creep into Hermes core | plugin-free: MCP + REST only, no plugins/memory/ changes (Footprint Ladder rung 2/3) |
11. Tam's acceptance checks (I'll run these)
ss -ltn | grep 8437→127.0.0.1:8437(loopback proof)- Cross-agent recall echo test (Atlas step 2) reproduced independently
- Row-count delta in
memory_longtermmatches writes issued mcp_solos_memory_*present in a live Hermes turn on thesolprofile- A2A ACK sweep unchanged
- Degraded-mode test: recall with no embedder returns
degraded: trueand still returns rows
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.
Truncation reconciliation — three competing 500 theories, which one is true
Tam's 22:56 receipt (task-81dd9762acb94ac5) claimed the 500-char cut is upstream of his door and that the effective threshold is above ~900 bytes. My addendum-8 commit (5eca1b1) claimed the cap was proven at the wire with raw byte capture. Those two statements look contradictory. They are not — but neither is complete, and the real answer is a third mechanism that explains both.

