# unified-agent-memory — Architecture

## What this is

A complete, self-deployable **shared memory system for multiple AI agents**.
One Obsidian vault is the single source of truth; every agent (dsh, Codex,
Claude Code, Hermes, anything that can run a Python CLI or read Markdown)
reads canonical notes and writes facts into a submission inbox. A
dependency-free Python core handles search, promotion, conflict adjudication,
forgetting and — optionally — semantic embedding, a concept graph and
session-digest extraction. **No agent runtime is required.**

The design fuses the strongest ideas from two open-source agent-memory
projects into the original vault-first model:

- from **rohitg00/agentmemory**: hybrid retrieval (BM25 + vectors + graph
  fused by weighted RRF), versioned supersession, memory typing with salience,
  token-budget output, access-reinforced forgetting;
- from **TencentDB-Agent-Memory**: the L0–L3 memory pyramid, per-agent memory
  records (type/importance/version/provenance), and BM25-by-default with
  embeddings as an optional enhancement.

## Layered view

```mermaid
flowchart TB
    subgraph Agents [agents - any mix]
        D[dsh plugin<br/>memory_search/show/submit/status]
        C[Codex / Claude<br/>AGENTS.md · CLAUDE.md + CLI]
        H[Hermes-style agent<br/>hooks + CLI]
    end

    subgraph Core [core - pure Python stdlib]
        M[memory.py<br/>init · search · show · submit · embed · digest · graph]
        P[promoter.py<br/>review · apply · adjudicate · supersede]
        F[forgetter.py<br/>salience × decay × reinforcement]
        HC[health.py<br/>doctor · gc]
        I[index.py<br/>index orchestration + public compatibility exports]
        SC[schema.py<br/>SQLite schema + per-vault connection lifecycle]
        MA[maintenance.py<br/>canonical repair + reversible backups]
        S[search.py<br/>hybrid: BM25 + vectors + graph, RRF]
        E[embed.py<br/>SiliconFlow Qwen3-Embedding-4B · optional]
        G[graph.py<br/>concept co-occurrence · optional]
        DG[digest.py<br/>session → facts · optional]
    end

    subgraph Vault [Obsidian vault - source of truth]
        CAN[(50-Agent-Context<br/>canonical notes)]
        INBOX[(Agent提交区<br/>write inbox)]
        SIT[(情境信息<br/>pending · conflicts)]
        FORG[(记忆遗忘区<br/>demoted, reversible)]
        ARCH[(会话归档 · Hermes会话自动归档)]
    end

    D -->|reads| CAN
    D -->|writes| INBOX
    C -->|CLI| M
    M --> I
    I --> SC
    P --> MA
    S --> I
    P -->|promotes/archives/supersedes| CAN
    P -->|conflicts| SIT
    F -->|demotes| FORG
    DG -->|facts| INBOX
    INBOX --> P
    ARCH --> DG
```

## Memory database (derived copy)

The SQLite index (`~/.unified-memory/index-<vault-hash>.db`) is a **derived
copy** of the vault, rebuilt incrementally from content digests. It holds:

`index.py` remains the public compatibility facade. SQLite schema creation,
migration and per-vault connections live in `schema.py`; canonical duplicate
and template cleanup lives in `maintenance.py`. The old public imports remain
re-exported so existing CLI, remote-index and integration callers do not need
to change.

| Table | Purpose |
|---|---|
| `docs` | per-note digest → incremental rebuild |
| `fts` | FTS5/BM25 over note content (legacy search) |
| `fts_mem` | per-line FTS5 for the BM25 retrieval stream |
| `memories` | one record per canonical fact line: `type`, `importance`, `status`, `version`, `superseded_by`, `source_agent`, `project`, `created_at`, `updated_at`, `access_count` |
| `embeddings` | Float32 vectors per memory line (optional) |
| `query_cache` | recent query vectors (7-day reuse; derived, prunable) |
| `access_log` | read receipts → reinforcement scoring |
| `graph_nodes` / `graph_edges` | optional concept co-occurrence graph |
| `audit` | provenance/accountability for mutations |

Memory `type` is rule-classified (preference / architecture / pattern / bug /
workflow / fact / other); `importance` is its salience (0.9 … 0.4). Lines
under a `已取代` heading are indexed as `superseded`.

## Memory pyramid (L0–L3)

| Layer | Vault location | Meaning |
|---|---|---|
| L0 | 会话归档/ · Hermes会话自动归档/ | raw session history |
| L1 | canonical note lines (memories table) | atomic durable facts |
| L2 | 情境信息/ | pending · conflicts · scenarios |
| L3 | 我的偏好摘要 · 工程执行规则 · UI审美准则 … | stable profiles & doctrine |

The pyramid is **conceptual**: physical files stay in the user's curated
layout; the index/model layer organizes content across layers.

## Lifecycle

```mermaid
flowchart TD
    A[Agent 提交: memory submit / Agent提交区/] --> B{凭据脱敏引擎}
    B -- 匹配 API Key / Token 模式 --> C[掩码清洗 REDACTED]
    B -- 校验通过 --> D[写入 50-Agent-Context/Agent提交区/]
    D --> E[更新 SQLite FTS5 本地索引]
    D --> F[Promoter 审核管线]
    F -- 事实无冲突 --> G[晋升至 50-Agent-Context/ canonical 笔记]
    F -- 存在矛盾 --> H[情境信息/事实冲突待裁决.md]
    H --> I[Adjudicator 裁决 decide / advise]
    I --> G
    G --> J[Forgetter 记忆衰减 · 每周一 04:00]
    J -- 触发衰减条件 --> K[记忆遗忘区/ 可逆降级]
```

The same flow in words:

1. **Write** — `memory submit` (or any file in `Agent提交区/`) → inbox.
2. **Digest (optional, default on)** — `digest.py` summarizes archived
   sessions into durable facts via a small LLM call and drops them into the
   inbox (redacted, credential-checked, cursor-idempotent).
   `--source state` additionally reads Hermes's `state.db` through
   `sessiondb.py`, which covers sessions that were never archived as Markdown.
   That adapter is deliberately one-way and read-only: it projects the database
   onto the same `(date, transcript)` contract the archive produces, opens the
   file with `mode=ro`, checks the columns it uses and names any that moved,
   and strips tool output before anything reaches the LLM. Each source keeps
   its own cursor, so switching between them never re-bills a date. See
   `docs/SESSIONDB.md` for the measured gap this closes and the reasons a
   schema dependency was avoided.
3. **Promote** — the promoter classifies (English + Chinese rules), dedups
   (char-bigram similarity), detects **supersession** (replacement cues such as
   “改用/迁移到/instead of” move the old line under a `已取代` section) and
   routes genuine contradictions to the conflict queue for human adjudication.
   Default is human-confirmed: `--review` → `--apply`; `--auto` is opt-in.
4. **Forget** — a line is demoted to `记忆遗忘区/` (reversible, never deleted)
   when `score = importance × (floor + decay) + access-reinforcement` drops
   below threshold, it is old enough, and it is not a durable/protected topic.
   High-importance knowledge (prefs/architecture/rules) persists indefinitely.
5. **Recall** — hybrid search fuses five streams (BM25, CJK bigram BM25,
   vectors, graph, trigram) with weighted RRF, diversifies per note, and caps
   output by a real token budget. Everything is wrapped in `<memory-data>`
   markers and redacted.
6. **Observe & maintain** — `memory doctor` reports index size, inbox backlog,
   conflicts, vector coverage, graph bloat, sync-conflict copies and orphan
   indexes; `memory gc` prunes only derived data (default dry-run).

## Interfaces

- **CLI** — `memory init|use|search|show|submit|status|embed|digest|graph|doctor|gc`;
  lifecycle CLIs `promoter` / `forgetter` / `catchup` (missed-run recovery)
- **dsh plugin** — `memory_search` (`hybrid` 开启三流融合检索；可选 `format`/`budget` 控制输出形态与预算),
  `memory_show`, `memory_submit`, `memory_status`; startup + hourly
  `unified_memory.catchup` replay of missed schedule runs
- **Update scheme** — boot-time reconcile (`upgrade.json` state vs this build:
  install when nothing recorded, repair when drifted, refresh when the version
  changed; idempotent, per-artifact) plus a declarative cleanup sweep
  (`upgrade-cleanup.ts`: retired-artifact list + generated-shape garbage
  rules, e.g. deploy backups keep newest 3); retiring a file in a future
  release is one data row, never new migration code
- **OS schedules** — `setup.py cron` registers daily promotion (03:00) and
  weekly forgetting (Mon 04:00); successful runs stamp
  `50-Agent-Context/调度状态.json`, shared across a synced fleet
- **Hermes integration** — `daily_cron.py` (promote/repair/forget),
  `archive_session.py`, `inject_context.py`
- **Remote index** (optional) — HTTP server exposing the same index with Bearer
  auth; client falls back to local on failure.

## Key design decisions

1. **Obsidian is the source of truth.** The index and every agent's cache are
   derived copies; conflicts resolve to the vault.
2. **Core is runtime-independent.** Pure Python stdlib; config via
   `UNIFIED_MEMORY_VAULT` env or `~/.unified-memory.yaml`.
3. **Local-first indexing.** SQLite on your own machine; embeddings are an
   optional semantic layer (SiliconFlow), stored locally, and BM25 remains the
   default when the provider is unavailable.
4. **Human-confirmed promotion by default.** `--review` builds the pending
   list; `--apply` executes it; conflicts are never silently overwritten;
   supersession keeps history under `已取代`.
5. **Multi-writer safe.** Vault-level file lock + atomic writes; the promoter
   re-checks dedup under the lock.
6. **Privacy at the boundary.** Credential-shaped text is redacted before it
   reaches the index, the LLM, or any output; submissions are rejected if they
   contain plaintext credentials.
7. **Prompt-injection guard.** Search output is wrapped in `<memory-data>`
   markers and every tool description states the content is data.
8. **Everything reversible.** Forgetting demotes to `记忆遗忘区/`; supersession
   moves old lines under `已取代`; migration backs up the vault first and is
   verified against a content baseline.
9. **Updates never erase learning.** Re-indexing a changed note preserves each
   unchanged line's access count, created date and embedding. A logic-version
   marker forces a one-time reparse after parser upgrades.
10. **Concurrent by default.** The index runs in WAL mode with a 5 s busy
    timeout, so searches/UI reads do not fail while promotion writes.
11. **Writer dialect tolerant.** Both the repo stamp and the Hermes
    `[[…]]`/`事件`/`｜有效至` stamp are parsed; `｜有效至` lines are superseded.
12. **Sync-client aware.** OneDrive/Dropbox conflict copies are excluded from
    the index and surfaced as an error by `doctor` instead of becoming a second
    source of truth.

## Runtime adapters and responsibility boundaries

The core package is deliberately separated from agent-specific adapters:

| Component | Responsibility | Writes canonical notes? |
|---|---|---|
| `core/` | index, search, promotion, conflicts, forgetting | Only the three writers: `promoter.promote`, `forgetter.demote`, `maintenance.repair_existing`, each under the vault `file_lock` |
| `src/` (DSH host half) | six dsh tools, status UI, read/show/search and inbox submission | No |
| `src/recall.ts` | optional pre-step recall and memory-discipline instructions | No |
| `Agent提交区/` | isolated cross-agent submission files | Submission files only |

### Hermes-side pipeline (external to this repo)

A full deployment also has a Hermes-owned pipeline in
`HERMES_HOME/scripts/`. It is not shipped by this package, but it must follow
the same hygiene contract:

| Script | Job | Shared rules applied |
|---|---|---|
| `memory_hygiene.py` | single source for versioned/conflict-copy/canonical-path rules, atomic write + backup | — |
| `dream_memory_promoter.py` | classify/dedup/supersede archive + inbox facts | skips history in conflicts; backs up before rewrite |
| `memory_forgetter.py` | weekly decay | canonical profiles only; never versioned history; backs up |
| `inject_hermes_memory_context.py` | per-turn context pack | drops history + provenance suffixes |
| `sync_obsidian_to_supermemory.py` / `sync_obsidian_to_mem0.py` | semantic-provider export | canonical notes only; history/sections/conflict copies filtered |
| `publish_openclaw_memory.py` | remote read-only snapshot | same filter |
| `hermes_memory.py` (skill) | read-only search for Codex/Claude/ZCode | current facts only; excludes inbox/forget/archives/copies |

The three components above ship as one package,
`dsh-unified-agent-memory`: `src/recall.ts` used to be a separate
`dsh-memory-discipline` plugin, and it was merged in so that recall shares the
tools' `vaultPath`/`pythonPath`/`corePath` instead of carrying a second
interpreter and a second hardcoded core path. Recall invokes the core CLI, never
the other way round; it does not replace the core, bypass review, or write the
canonical vault directly.

## Migration & verification

Migration is reversible and content-preserving:

1. Back up `50-Agent-Context/` and record a baseline manifest (sha256 per file).
2. Rebuild the memory database: `memory status` (or any search) creates/upgrades
   the per-vault index from the vault.
3. Enrich embeddings (optional): `memory embed`.
4. Verify: the baseline manifest is unchanged (no content touched) and
   `memory search` / `memory search --hybrid` return at least what they did
   before.

The CLI commands are idempotent and incremental (content-digest based), so the
whole procedure can be re-run at any time without side effects.

## Compatibility

- DSH `0.2.0-rc.2` / `0.2.1-rc.2` / `0.2.1-rc.3` plugin (Web and Desktop
  profiles); Python ≥ 3.10 for
  the core; SQLite FTS5 when available (substring fallback otherwise).
- SiliconFlow embeddings: `Qwen/Qwen3-Embedding-4B`, 1024 dims (optional; API
  key in `~/.unified-memory/secrets.yaml`, permissions 0600 / user-only ACL).
