Young's Mind
Tech

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)

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:

TableRowsShapeUse
memory_durable30agent, key, value, categorycanonical identity/persona — read-mostly, not this service's write target
memory_longterm321agent, topic, fact, source, confidence, embedding BLOBdurable facts + semantic recall
memory_episodic1,495agent, event_type, summary, details, outcome, durationturn/event timeline
memories230agent, key, value, tags, sourcelegacy 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.db schema in v1 — additive only, no migrations, no column changes. Read the four tables above; write only to memory_longterm + memory_episodic (+ a new memory_short table, see §5).
  • No vector-DB migration. v1 embeds reuse the existing embedding BLOB 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)

ToolArgsReturns
remember_shortagent, key, value, ttl_s=86400, tags=[]{stored: bool, key, expires_at}
remember_longagent, topic, fact, source, confidence=0.5, tags=[]{id, topic, embedded: bool}
remember_eventagent, event_type, summary, details, outcome, duration_s{id}
recallagent, query, k=5, scope="shared"|"own", tiers=["short","long","episodic"]{results:[{tier, agent, text, score, source}], degraded: bool}
forgetagent, key{deleted: int}
promoteagent, 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=own restricts to the caller's rows.
  • memory_durable is NOT exposed for recall by default (identity/persona is Sol's canon). A tiers flag may include it only for the requesting agent's own persona/ 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 recall is not required, but the service MUST return agent on 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/stats so a silent-failure sweep can see it.

8. Rollout (Atlas)

  1. Unit + /health up on 127.0.0.1:8437, loopback only, verified with curl -sS http://127.0.0.1:8437/health = 200 and ss -ltn | grep 8437 showing 127.0.0.1 (not 0.0.0.0).
  2. REST proof, out-of-band: POST /v1/long as agent atlas, then GET /v1/recall?agent=sol&q=… and confirm the atlas-authored row comes back to a different agent. This is the acceptance test for the sharing rule.
  3. A2A mesh untouched: ACK sweep :8300-:8307 (excluding the already-failed :8305 Alpha) must stay 200 before and after. A memory service that costs the mesh a port is a failed change.
  4. Wire Hermes profiles (sol, min, mira, and the default root on LAB): add
    mcp_servers:
      solos-memory:
        url: "http://127.0.0.1:8437/mcp"
        timeout: 60
        connect_timeout: 30
    
    then hermes gateway restart from a separate shell (never from inside a gateway turn — the restart kills the command running it) and confirm the mcp_solos_memory_* tools appear in the next turn's toolset.
  5. Wire the A2A agents: each run_.py gains a tiny HTTP client call to 127.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).
  6. 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) into memory_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

RiskMitigation
Service competes with agents for the shared 422 MB solos.dbWAL + short transactions + read-only pool; mesh must stay 200 throughout (Atlas step 3)
A memory write becomes a silent failure nobody seesexplicit 503s + /v1/stats counters + no-op-on-error in the A2A client
Durable/persona data leaks into shared recallmemory_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 coreplugin-free: MCP + REST only, no plugins/memory/ changes (Footprint Ladder rung 2/3)

11. Tam's acceptance checks (I'll run these)

  1. ss -ltn | grep 8437 → 127.0.0.1:8437 (loopback proof)
  2. Cross-agent recall echo test (Atlas step 2) reproduced independently
  3. Row-count delta in memory_longterm matches writes issued
  4. mcp_solos_memory_* present in a live Hermes turn on the sol profile
  5. A2A ACK sweep unchanged
  6. Degraded-mode test: recall with no embedder returns degraded: true and still returns rows
Copyright © 2026