Agent Memory — Agent Memory / Learning / Evolution Infrastructure (Implementation Spec)
Formerly named Hermes Lite (born from a review and rewrite of the Hermes v2.1 five-layer memory guide). This system has long outgrown "lite Hermes" — it is a general memory, automatic-learning, and self-evolution foundation for all agents, hence renamed agent-memory. The historical name is kept in the docs for traceability.
0. Background & Design Philosophy
This system stems from a review and correction of the "Hermes v2.1 five-layer memory guide". The original guide had 40-50% stub code, fabricated core metrics (recall always returned 95-97%), plaintext-stored passwords, and a YAML that was never loaded. This spec keeps its design ideas while everything is implemented as real, runnable code.
Core principles (must be followed):
- No Execution, No Memory:
execution_verified defaults to False; unverified memories are pruned after 7 days; the verify command confirms manually.
- Secrets never hit disk: passwords/tokens/API keys are auto-redacted to
[已脱敏:type]; writing the raw value to any file is a defect.
- Single source of configuration: all rooms/mappings/retrieval params live only in
config.yaml; the code loads it; hardcoded duplicates are forbidden.
- No fabricated metrics: every statistic must be a real count; never output "estimated recall" pinned to a fixed range.
- Zero-dependency first: core capabilities (BM25/storage/pruning) are pure standard library; external services (vector/LLM/Rerank) are pluggable backends with circuit-breaker degradation.
1. Target Shape
Sediment digest (LLM summarization preferred / rule-based fallback) + conflict detection
Write remember (redact → dedup-merge → conflict warning → unverified by default → graph update)
Storage 5 rooms (prefs/decisions/configs/projects/events) + Obsidian Markdown
+ atomic writes + daily auto-backup + optional AES encryption
Retrieve BM25 (pure Python) → vectors (local hash / siliconflow BGE-M3 / jina v3)
→ RRF fusion → Rerank (BGE-reranker-v2-m3) → hot/cold weighting
Lifecycle Access tracking + adaptive importance (every 10 accesses +1, cap 10) + prune/archive + critical memories kept forever
Associate Memory graph (entity ↔ memory, auto-maintained)
Workflow auto_sediment.py (sediment from session logs) + full CLI
2. Directory Layout
agent-memory/ # Project dir name (matches the reference implementation; no functional impact, can be customized)
├── hermes_lite.py # Single-file core (CLI + library), all classes here
├── config.yaml # Single configuration source
├── test_hermes_lite.py # End-to-end tests (≥78 assertions)
├── test_stress.py # Stress test (150 memories)
├── auto_sediment.py # Session logs → auto-sedimentation
├── README.md
└── data/ # Runtime data (set via --root or $HERMES_LITE_ROOT)
├── config.yaml # Copied into the data root (or loaded by the code)
├── .env # Keys (permissions 0600): JINA_API_KEY / SILICONFLOW_API_KEY / DEEPSEEK_API_KEY
├── index.json # Memory index (access counts, verification flags)
├── vectors.json # Vector cache {"backend": name, "vectors": {id: vec}}
├── graph.json # Memory graph {"entities": {name: {type, memory_ids}}}
├── .vector_backend_state.json # Circuit-breaker state (records remote failures)
├── .master_key # AES master key (auto-generated when ENCRYPT=on, permissions 0600)
├── rooms/ # Room descriptions
├── store/ # Memory bodies (Obsidian-compatible .md, YAML frontmatter)
└── backups/ # Prune archives + daily auto-backups
3. Implementation Steps (execute in order, run tests after each phase)
Phase 1: Storage layer + Entry layer
HermesLite class: __init__(root) creates directories, loads YAML config (yaml.safe_load, falls back to embedded defaults), loads index.json.
remember(text, category, importance, verified, source): unverified by default; _redact() redacts secrets (regexes in §5/§9.5); _find_duplicate() merges into the existing entry when similarity ≥0.85 (max importance, no new entry); _find_conflicts() detects opposing assertions; writes store/{id}.md (YAML frontmatter) + index.json; _invalidate_bm25().
verify(memory_id): sets execution_verified=True.
- Memory ID format:
mem_YYYYMMDD_HHMMSS_xxxx.
Phase 2: Lifecycle
- Room retention: prefs 365 / decisions 730 / configs 365 / projects 545 / events 90 days (from config.yaml).
- Prune
prune(dry_run): critical memories (importance=10) kept forever; unverified 7 days; low-frequency (access <3 and >30 days); expired ones are archived to backups/prune_*.json then deleted. Must truly find and delete — no stubs.
stats(): total / room distribution / verified count / hot-cold / avg importance / bytes (all real).
- Atomic writes: temp file +
os.replace; first write of the day auto-backups (keep 7 copies).
Phase 3: Retrieval (BM25 + vectors + RRF)
tokenize_terms(text): ASCII words (lowercased) + Chinese 2/3-grams.
BM25Index: pure Python Okapi BM25 (k1=1.5, b=0.75); search(query) returns all docs in descending order; _keyword_rank filters by room, hot memories (access_count ≥ threshold) get ×1.1.
- Pluggable vector backends:
LocalHashVectorizer: zero-dependency fallback, character n-gram MD5 hashing → 2048-dim normalized vectors.
JinaVectorizer (https://api.jina.ai/v1/embeddings, jina-embeddings-v3, 1024-dim, international network).
SiliconFlowVectorizer (https://api.siliconflow.cn/v1/embeddings, BAAI/bge-m3, 1024-dim, mainland-China direct; queries prefixed with "为这个句子生成表示以用于检索相关文章:").
- Key source: environment variable >
data/.env; backend selection HERMES_VECTOR_BACKEND=auto|local|jina|siliconflow; auto prefers jina (if key present), then siliconflow, then local.
- Circuit breaker: one remote failure → persist to
.vector_backend_state.json → degrade to local and retry once with local to fill vectors; delete the state file or set the backend explicitly to recover.
- Backend-switch cache rebuild: vectors.json carries a backend tag; switching backends (different dims/semantics) clears the cache and regenerates.
recall(query, room, top_k): keyword ranking + vector recall (cosine ≥ threshold: local 0.30 / remote 0.35, filtered by room) → rrf_merge([kw_ids, vec_ids]) fusion. Results include kw_score/vec_score.
Phase 4: Rerank + Graph + Encryption
SiliconFlowReranker (https://api.siliconflow.cn/v1/rerank, BAAI/bge-reranker-v2-m3): re-ranks the top-20 candidates after RRF → top-k; on failure, in-process circuit breaker (_rerank_ok=False), base retrieval unaffected; disable with HERMES_RERANK=off.
GraphStore (graph.json): extract_entities() rule-based extraction (uppercase-initial English words minus a stopword list + version numbers \d+\.\d+ / v\d+\.\d+); incrementally maintained on remember/merge/delete; CLI: graph / graph <entity> / graph --rebuild.
- Encryption (
get_encryption): with HERMES_ENCRYPT=on, Fernet AES encrypts index/vectors/graph; key from HERMES_MASTER_KEY or auto-generated data/.master_key (0600); store/*.md stays plaintext (redaction already protects); degrades gracefully if cryptography is missing. Off by default.
Phase 5: Auto-sedimentation + Workflow
RuleDigester (zero-dependency fallback): regex extraction of decisions/preferences/configs/projects/events, format "label: value".
LLMDigester (OpenAI-compatible chat completions, default DeepSeek https://api.deepseek.com, deepseek-chat): system prompt requires a JSON array output (content/category/importance); secrets must not be output in plaintext; response_format: json_object; records last_usage (real token cost); falls back to rule-based on failure.
HermesLite.digest(text, apply): extract → preview → with --apply, each item goes through the full remember chain (unverified by default).
auto_sediment.py: parses DSH session logs (user/message + assistant/message text, skipping reasoning/tool results; .zstd decompressed via zstd -dc; --latest auto-finds the newest session) → digest; --dry-run preview / --apply writes.
Phase 6: Tests & Acceptance
test_hermes_lite.py: ≥78 assertions covering: config loading, redaction (plaintext never on disk), dedup-merge, conflict detection, verification principle, pruning (incl. critical memories kept forever), BM25 hits/zero-hits, vector cache / backend switch / circuit breaker, RRF, Rerank breaker, graph, encrypted read/write consistency, digest preview/apply.
test_stress.py: 150 unique-content memories (avoid dedup-merge): write ≤10ms/item, retrieve ≤100ms/query, hit rate 10/10 (printed reference values, not hard assertions — see §7.2).
- Acceptance demos: a semantic query with no literal overlap hits the right memory;
grep -r "明文密码" data/ is empty; prune --dry-run reports real expired counts.
4. CLI Command Reference
remember TEXT [--category preference|decision|config|project|event|discussion|idea|general]
[--importance 1-10] [--verified] [--source S]
recall QUERY [--room R] [--top-k N] # BM25+vector+RRF+Rerank hybrid retrieval
vsearch QUERY [--top-k N] # pure vector retrieval (shows cosine)
verify ID # perform verification
prune [--dry-run] [--room R] [--yes] # prune (archive+delete; --dry-run counts only; --yes for confirmation prompt)
forget ID [--yes] # delete single item (--yes is a compat flag; deletion is immediate)
digest TEXT|--file F [--apply] [--max N] [--verified] # auto-sedimentation
graph [entity] [--top N] [--rebuild] # memory graph
stats # real statistics (incl. hot/cold)
rooms # room list
vectors # vector backend status (incl. breaker/Rerank state)
5. Key Design Decisions (do not omit when reproducing)
| Decision |
Rationale |
| Full set of 5 redaction regexes in §9.5, incl. copula "是/为" branches |
Measured: with the old regex, "密码是 hunter2" captured only "是" and let the password through; value capture uses ([^\s,,。;;]+) to avoid swallowing punctuation |
| Dedup-merge threshold 0.85 (SequenceMatcher) |
Prevents memory bloat; short contents excluded (len<5) |
| Conflict detection pos/neg regexes in §9.6 |
Rule-based is zero-cost; LLM detection is better but calling an API on every write is too expensive |
| BM25 instead of heuristic scoring |
More sensitive to term frequency / doc length / rare terms; measurably better discrimination (exact-query score 5→25) |
| RRF fusion (k=60) instead of score weighting |
The two score scales differ; rank-based fusion is more robust |
| Vector threshold filtering (0.30/0.35) instead of full recall |
Prevents irrelevant queries from being polluted by vector recall |
| Adaptive importance +1 every 10 accesses |
Frequently used memories naturally rise; cap 10 preserves critical-memory semantics |
| AES off by default |
Threat model: data already redacted + 0600 perms; encryption adds key-loss risk and makes files non-greppable |
| Graph auto-maintained, not used for ranking |
With small memory volumes the graph has no network effect; keeping it costs nothing; enhance when scale grows |
6. Environment Adaptation (important)
- Network partitioning: international APIs (jina/openai/huggingface) may be unreachable; mainland-China services (siliconflow/dashscope/deepseek) reachable. The 3 backends + circuit breaker exist precisely for this.
- Sandbox: file writes may be restricted to a designated workspace; the data root must live in a writable area (e.g.
agent-memory/data under the workspace), set via $HERMES_LITE_ROOT.
- Key security: all keys go only into
data/.env (chmod 600); never hardcode into code, never print to any output.
- Token cost transparency: each digest run costs ~250 fixed system-prompt tokens + input + output (DeepSeek measured ~490 tokens per run / 5 points); retrieval injects top-5 ≈ 300-1000 tokens/round, and this does NOT grow linearly with total memory volume.
7. Acceptance Criteria (all must hold)
python3 test_hermes_lite.py all pass (≥78).
python3 test_stress.py: 150 memories, exact query hits. Timing values (write ≤10ms/item, retrieve ≤100ms/query) are printed reference values, not hard assertions (to avoid flakiness on slow machines); "10/10 hit rate" means all 10 queries returned results.
- Demonstrate three things:
- Semantic retrieval: a query with no literal overlap hits — requires a remote vector key (siliconflow/jina); without keys, local hashing only demonstrates lexical approximation (synonyms won't match); state the backend during acceptance.
- Redaction: after writing "密码是 xxx",
grep -r xxx data/ finds nothing.
- Lifecycle: unverified memories pruned at 7 days, critical memories kept forever (test assertions).
- All statistics are real counts (no fabricated metrics).
- Core functionality works with no third-party libraries beyond the standard library (BM25/storage/retrieval/pruning).
8. Reference Implementation
The complete reference implementation for this skill lives in the workspace agent-memory/ (hermes_lite.py single file ~1600 lines, 79 tests, 150-memory stress test all passing). Reproduce from this spec; use the reference only to cross-check extreme details inconsistent with this appendix. After implementing, read Token节省实测分析报告v2.md to understand the system's token-cost model.
9. Data Formats & Configuration Reference (Appendix — implement exactly)
9.1 store/*.md format (Obsidian-compatible, YAML frontmatter)
---
id: mem_20260817_033106_ab12
category: decision
room: decisions
importance: 8
execution_verified: true
access_count: 3
last_accessed: 2026-08-17T04:00:00+00:00
source: 对话流自动沉淀
created_at: 2026-08-17T03:31:06+00:00
redacted: password,api_key
---
正文内容(已脱敏)
Fixed frontmatter field order: id, category, room, importance, execution_verified, access_count, last_accessed, source, created_at, redacted (redacted only when redaction occurred, comma-separated).
9.2 index.json entry schema
{
"id": "mem_20260817_033106_ab12",
"content": "脱敏后的记忆文本",
"category": "decision",
"room": "decisions",
"importance": 8,
"execution_verified": false,
"access_count": 0,
"last_accessed": null,
"source": null,
"created_at": "2026-08-17T03:31:06+00:00",
"redacted": []
}
index.json top level is an array of entries; writes use atomic write (temp file + os.replace) + daily first-write auto-backup to backups/auto_index.json.YYYYMMDD.bak (keep 7). Prune archive format: backups/prune_YYYYMMDD_HHMMSS.json = {"archived_at": ISO-time, "memories": [full entries...]}.
9.3 config.yaml complete reference values
rooms: # retention in days; display fields name/description/icon can be customized
prefs: 365 # preferences
decisions: 730 # decisions
configs: 365 # configs
projects: 545 # projects
events: 90 # events
category_mapping:
preference: prefs decision: decisions config: configs
project: projects event: events discussion: events idea: events general: events
retrieval:
default_top_k: 5 min_score: 0.5 time_decay_days: 90
weight_importance: 0.4 weight_frequency: 0.5 weight_verified: 0.15
boost_verified: 1.15 hot_threshold: 5 hot_boost: 1.1
pruning:
unverified_retention_days: 7 low_frequency_days: 30
low_frequency_threshold: 3 critical_importance: 10 default_retention_days: 90
security:
redact_patterns: (see 9.5)
Note: weight_importance/weight_frequency/weight_verified are legacy heuristic params; the actual retrieval path uses BM25+RRF+hot boost+min_score. These weight fields are kept for compatibility but are not used by retrieval.
9.4 DSH session log structure (auto_sediment parse target)
{"type":"user/message","seq":5,"time":1786932396089,"data":{"content":[{"type":"text","text":"user message"}]}}
{"type":"assistant/message","seq":6,"time":...,"data":{"message":{"role":"assistant","content":[{"type":"reasoning","text":"reasoning (skip)"},{"type":"text","text":"assistant reply"}]}}}
{"type":"tool/call","seq":7,"time":...,"data":{"name":"bash","arguments":"{\"command\":\"...\"}"}}
{"type":"tool/result","seq":8,"time":...,"data":{"message":{"source":{"kind":"tool"},"content":[{"type":"tool-result","toolCallId":"...","content":[{"type":"text","text":"tool output (skip)"}]}]}}}
Extraction rules: user/message → data.content[].text; assistant/message → data.message.content[] where type=="text" (skip reasoning); skip tool/call and tool/result. Join as "用户: …\n助手: …", truncate to 8000 chars, feed to digest. .zstd-compressed session files are decompressed via zstd -dc to stdout before parsing.
9.5 Redaction regexes (complete set of 5, label inference)
# pattern → redaction label
r"((?:密码|口令|passwd|password)(?:\s*(?:是|为|[::=])\s*|\s+))([^\s,,。;;]+)" # password
r"((?:api[_-]?key|apikey|access[_-]?key|密钥)(?:\s*(?:是|为|[::=])\s*|\s+))([^\s,,。;;]+)" # api_key
r"((?:token|secret|授权码)(?:\s*(?:是|为|[::=])\s*|\s+))([^\s,,。;;]+)" # token
r"(sk-[A-Za-z0-9_\-]{8,})" # api_key
r"(Bearer\s+[A-Za-z0-9_\-\.]{10,})" # token
Replacement logic: when a prefix capture group (group 1) exists, keep the prefix and replace the value with [已脱敏:label]; with no prefix group, replace the whole match with [已脱敏:label]. The decision is based on capture-group count ≥2 (prefix+value patterns) before keeping the prefix; single-group patterns (sk-xxx / Bearer xxx) must be fully replaced — ⚠️ an older reference version kept the whole match as the "prefix" causing plaintext leakage (sk-abc123secret[已脱敏:api_key]); do NOT copy that logic, follow this appendix (fixed and test-covered). Value capture uses ([^\s,,。;;]+) (not \S+, to avoid swallowing Chinese punctuation). Execute top-to-bottom; record each label into the redacted field. Critical: copula forms like "密码是 hunter2" must be redacted — the (?:\s*(?:是|为|[::=])\s*|\s+) branch covers "密码:x", "密码是 x", and "密码 x".
9.6 Conflict-detection regexes
pos = r"(?:决定|选择|采用|使用|选用|用)\s*(?:了|的)?\s*([A-Za-z0-9_\-.]+)" # positive assertion
neg = r"(?:放弃|不用|弃用|反对|拒绝|移除|取消|停止)\s*(?:了|的)?\s*([A-Za-z0-9_\-.]+)" # negative assertion
New text's neg ∩ old memory's pos (or the reverse) → conflict, output {memory_id, entity, type, content}.
9.7 recall scoring details
- Keyword score kw_score = BM25 score (>0 is a candidate); hot memories (access_count ≥ hot_threshold) get ×hot_boost.
- Vector score vec_score = cosine (only enters fusion when ≥ threshold: local 0.30 / remote 0.35, filtered by room).
- Fusion:
rrf_merge([kw_ids, vec_ids], k=60).
- display_score = kw_score (when >0) else vec_score×3; drop when kw=0 and vec < min_score/3.
- Each hit updates entry.access_count+=1, last_accessed,
_save_index(); when access_count % 10 == 0 and importance<10, importance+1.
1---2name: agent-memory-en3description: Build "Agent Memory" — a universal infrastructure giving any AI agent long-term memory, automatic learning, and self-evolution: cross-session memory, conversation auto-sedimentation (digest), hybrid semantic retrieval (BM25 + vector + Rerank), memory graph, pruning, and encryption. Use this skill when the user asks to implement/reproduce/build a memory system, or needs long-term memory, conversation sedimentation, semantic retrieval, knowledge accumulation, or self-evolution capabilities. Contains the complete implementation spec, key design decisions, and acceptance criteria — a new harness instance can reproduce it step by step from this document alone, no further explanation needed.4license: MIT5---67# Agent Memory — Agent Memory / Learning / Evolution Infrastructure (Implementation Spec)89> Formerly named **Hermes Lite** (born from a review and rewrite of the Hermes v2.1 five-layer memory guide). This system has long outgrown "lite Hermes" — it is a general memory, automatic-learning, and self-evolution foundation for **all agents**, hence renamed `agent-memory`. The historical name is kept in the docs for traceability.1011## 0. Background & Design Philosophy1213This system stems from a review and correction of the "Hermes v2.1 five-layer memory guide". The original guide had 40-50% stub code, fabricated core metrics (recall always returned 95-97%), plaintext-stored passwords, and a YAML that was never loaded. This spec keeps its **design ideas** while everything is implemented as **real, runnable code**.1415Core principles (must be followed):161. **No Execution, No Memory**: `execution_verified` defaults to `False`; unverified memories are pruned after 7 days; the `verify` command confirms manually.172. **Secrets never hit disk**: passwords/tokens/API keys are auto-redacted to `[已脱敏:type]`; writing the raw value to any file is a defect.183. **Single source of configuration**: all rooms/mappings/retrieval params live only in `config.yaml`; the code loads it; hardcoded duplicates are forbidden.194. **No fabricated metrics**: every statistic must be a real count; never output "estimated recall" pinned to a fixed range.205. **Zero-dependency first**: core capabilities (BM25/storage/pruning) are pure standard library; external services (vector/LLM/Rerank) are pluggable backends with circuit-breaker degradation.2122## 1. Target Shape2324```25Sediment digest (LLM summarization preferred / rule-based fallback) + conflict detection26Write remember (redact → dedup-merge → conflict warning → unverified by default → graph update)27Storage 5 rooms (prefs/decisions/configs/projects/events) + Obsidian Markdown28 + atomic writes + daily auto-backup + optional AES encryption29Retrieve BM25 (pure Python) → vectors (local hash / siliconflow BGE-M3 / jina v3)30 → RRF fusion → Rerank (BGE-reranker-v2-m3) → hot/cold weighting31Lifecycle Access tracking + adaptive importance (every 10 accesses +1, cap 10) + prune/archive + critical memories kept forever32Associate Memory graph (entity ↔ memory, auto-maintained)33Workflow auto_sediment.py (sediment from session logs) + full CLI34```3536## 2. Directory Layout3738```39agent-memory/ # Project dir name (matches the reference implementation; no functional impact, can be customized)40├── hermes_lite.py # Single-file core (CLI + library), all classes here41├── config.yaml # Single configuration source42├── test_hermes_lite.py # End-to-end tests (≥78 assertions)43├── test_stress.py # Stress test (150 memories)44├── auto_sediment.py # Session logs → auto-sedimentation45├── README.md46└── data/ # Runtime data (set via --root or $HERMES_LITE_ROOT)47 ├── config.yaml # Copied into the data root (or loaded by the code)48 ├── .env # Keys (permissions 0600): JINA_API_KEY / SILICONFLOW_API_KEY / DEEPSEEK_API_KEY49 ├── index.json # Memory index (access counts, verification flags)50 ├── vectors.json # Vector cache {"backend": name, "vectors": {id: vec}}51 ├── graph.json # Memory graph {"entities": {name: {type, memory_ids}}}52 ├── .vector_backend_state.json # Circuit-breaker state (records remote failures)53 ├── .master_key # AES master key (auto-generated when ENCRYPT=on, permissions 0600)54 ├── rooms/ # Room descriptions55 ├── store/ # Memory bodies (Obsidian-compatible .md, YAML frontmatter)56 └── backups/ # Prune archives + daily auto-backups57```5859## 3. Implementation Steps (execute in order, run tests after each phase)6061### Phase 1: Storage layer + Entry layer62- `HermesLite` class: `__init__(root)` creates directories, loads YAML config (`yaml.safe_load`, falls back to embedded defaults), loads index.json.63- `remember(text, category, importance, verified, source)`: **unverified by default**; `_redact()` redacts secrets (regexes in §5/§9.5); `_find_duplicate()` merges into the existing entry when similarity ≥0.85 (max importance, no new entry); `_find_conflicts()` detects opposing assertions; writes store/{id}.md (YAML frontmatter) + index.json; `_invalidate_bm25()`.64- `verify(memory_id)`: sets `execution_verified=True`.65- Memory ID format: `mem_YYYYMMDD_HHMMSS_xxxx`.6667### Phase 2: Lifecycle68- Room retention: prefs 365 / decisions 730 / configs 365 / projects 545 / events 90 days (from config.yaml).69- Prune `prune(dry_run)`: critical memories (importance=10) kept forever; unverified 7 days; low-frequency (access <3 and >30 days); expired ones are archived to backups/prune_*.json then deleted. **Must truly find and delete — no stubs.**70- `stats()`: total / room distribution / verified count / hot-cold / avg importance / bytes (all real).71- Atomic writes: temp file + `os.replace`; first write of the day auto-backups (keep 7 copies).7273### Phase 3: Retrieval (BM25 + vectors + RRF)74- `tokenize_terms(text)`: ASCII words (lowercased) + Chinese 2/3-grams.75- `BM25Index`: pure Python Okapi BM25 (k1=1.5, b=0.75); `search(query)` returns all docs in descending order; `_keyword_rank` filters by room, hot memories (access_count ≥ threshold) get ×1.1.76- Pluggable vector backends:77 - `LocalHashVectorizer`: zero-dependency fallback, character n-gram MD5 hashing → 2048-dim normalized vectors.78 - `JinaVectorizer` (`https://api.jina.ai/v1/embeddings`, jina-embeddings-v3, 1024-dim, international network).79 - `SiliconFlowVectorizer` (`https://api.siliconflow.cn/v1/embeddings`, BAAI/bge-m3, 1024-dim, mainland-China direct; queries prefixed with "为这个句子生成表示以用于检索相关文章:").80 - Key source: environment variable > `data/.env`; backend selection `HERMES_VECTOR_BACKEND=auto|local|jina|siliconflow`; auto prefers jina (if key present), then siliconflow, then local.81 - **Circuit breaker**: one remote failure → persist to `.vector_backend_state.json` → degrade to local and retry once with local to fill vectors; delete the state file or set the backend explicitly to recover.82 - **Backend-switch cache rebuild**: vectors.json carries a backend tag; switching backends (different dims/semantics) clears the cache and regenerates.83- `recall(query, room, top_k)`: keyword ranking + vector recall (cosine ≥ threshold: local 0.30 / remote 0.35, filtered by room) → `rrf_merge([kw_ids, vec_ids])` fusion. Results include kw_score/vec_score.8485### Phase 4: Rerank + Graph + Encryption86- `SiliconFlowReranker` (`https://api.siliconflow.cn/v1/rerank`, BAAI/bge-reranker-v2-m3): re-ranks the top-20 candidates after RRF → top-k; on failure, in-process circuit breaker (`_rerank_ok=False`), base retrieval unaffected; disable with `HERMES_RERANK=off`.87- `GraphStore` (graph.json): `extract_entities()` rule-based extraction (uppercase-initial English words minus a stopword list + version numbers `\d+\.\d+` / `v\d+\.\d+`); incrementally maintained on remember/merge/delete; CLI: `graph` / `graph <entity>` / `graph --rebuild`.88- Encryption (`get_encryption`): with `HERMES_ENCRYPT=on`, Fernet AES encrypts index/vectors/graph; key from `HERMES_MASTER_KEY` or auto-generated `data/.master_key` (0600); store/*.md stays plaintext (redaction already protects); degrades gracefully if cryptography is missing. **Off by default.**8990### Phase 5: Auto-sedimentation + Workflow91- `RuleDigester` (zero-dependency fallback): regex extraction of decisions/preferences/configs/projects/events, format "label: value".92- `LLMDigester` (OpenAI-compatible chat completions, default DeepSeek `https://api.deepseek.com`, deepseek-chat): system prompt requires a JSON array output (content/category/importance); secrets must not be output in plaintext; `response_format: json_object`; records `last_usage` (real token cost); falls back to rule-based on failure.93- `HermesLite.digest(text, apply)`: extract → preview → with `--apply`, each item goes through the full remember chain (unverified by default).94- `auto_sediment.py`: parses DSH session logs (`user/message` + `assistant/message` text, skipping reasoning/tool results; `.zstd` decompressed via `zstd -dc`; `--latest` auto-finds the newest session) → digest; `--dry-run` preview / `--apply` writes.9596### Phase 6: Tests & Acceptance97- `test_hermes_lite.py`: ≥78 assertions covering: config loading, redaction (plaintext never on disk), dedup-merge, conflict detection, verification principle, pruning (incl. critical memories kept forever), BM25 hits/zero-hits, vector cache / backend switch / circuit breaker, RRF, Rerank breaker, graph, encrypted read/write consistency, digest preview/apply.98- `test_stress.py`: 150 unique-content memories (avoid dedup-merge): write ≤10ms/item, retrieve ≤100ms/query, hit rate 10/10 (**printed reference values, not hard assertions** — see §7.2).99- Acceptance demos: a semantic query with no literal overlap hits the right memory; `grep -r "明文密码" data/` is empty; `prune --dry-run` reports real expired counts.100101## 4. CLI Command Reference102103```104remember TEXT [--category preference|decision|config|project|event|discussion|idea|general]105 [--importance 1-10] [--verified] [--source S]106recall QUERY [--room R] [--top-k N] # BM25+vector+RRF+Rerank hybrid retrieval107vsearch QUERY [--top-k N] # pure vector retrieval (shows cosine)108verify ID # perform verification109prune [--dry-run] [--room R] [--yes] # prune (archive+delete; --dry-run counts only; --yes for confirmation prompt)110forget ID [--yes] # delete single item (--yes is a compat flag; deletion is immediate)111digest TEXT|--file F [--apply] [--max N] [--verified] # auto-sedimentation112graph [entity] [--top N] [--rebuild] # memory graph113stats # real statistics (incl. hot/cold)114rooms # room list115vectors # vector backend status (incl. breaker/Rerank state)116```117118## 5. Key Design Decisions (do not omit when reproducing)119120| Decision | Rationale |121|---|---|122| Full set of 5 redaction regexes in §9.5, incl. copula "是/为" branches | Measured: with the old regex, "密码是 hunter2" captured only "是" and let the password through; value capture uses `([^\s,,。;;]+)` to avoid swallowing punctuation |123| Dedup-merge threshold 0.85 (SequenceMatcher) | Prevents memory bloat; short contents excluded (len<5) |124| Conflict detection pos/neg regexes in §9.6 | Rule-based is zero-cost; LLM detection is better but calling an API on every write is too expensive |125| BM25 instead of heuristic scoring | More sensitive to term frequency / doc length / rare terms; measurably better discrimination (exact-query score 5→25) |126| RRF fusion (k=60) instead of score weighting | The two score scales differ; rank-based fusion is more robust |127| Vector threshold filtering (0.30/0.35) instead of full recall | Prevents irrelevant queries from being polluted by vector recall |128| Adaptive importance +1 every 10 accesses | Frequently used memories naturally rise; cap 10 preserves critical-memory semantics |129| AES off by default | Threat model: data already redacted + 0600 perms; encryption adds key-loss risk and makes files non-greppable |130| Graph auto-maintained, not used for ranking | With small memory volumes the graph has no network effect; keeping it costs nothing; enhance when scale grows |131132## 6. Environment Adaptation (important)133134- **Network partitioning**: international APIs (jina/openai/huggingface) may be unreachable; mainland-China services (siliconflow/dashscope/deepseek) reachable. The 3 backends + circuit breaker exist precisely for this.135- **Sandbox**: file writes may be restricted to a designated workspace; the data root must live in a writable area (e.g. `agent-memory/data` under the workspace), set via `$HERMES_LITE_ROOT`.136- **Key security**: all keys go only into `data/.env` (chmod 600); never hardcode into code, never print to any output.137- **Token cost transparency**: each digest run costs ~250 fixed system-prompt tokens + input + output (DeepSeek measured ~490 tokens per run / 5 points); retrieval injects top-5 ≈ 300-1000 tokens/round, and this does NOT grow linearly with total memory volume.138139## 7. Acceptance Criteria (all must hold)1401411. `python3 test_hermes_lite.py` all pass (≥78).1422. `python3 test_stress.py`: 150 memories, exact query hits. Timing values (write ≤10ms/item, retrieve ≤100ms/query) are **printed reference values, not hard assertions** (to avoid flakiness on slow machines); "10/10 hit rate" means all 10 queries returned results.1433. Demonstrate three things:144 - Semantic retrieval: a query with no literal overlap hits — **requires a remote vector key** (siliconflow/jina); without keys, local hashing only demonstrates lexical approximation (synonyms won't match); state the backend during acceptance.145 - Redaction: after writing "密码是 xxx", `grep -r xxx data/` finds nothing.146 - Lifecycle: unverified memories pruned at 7 days, critical memories kept forever (test assertions).1474. All statistics are real counts (no fabricated metrics).1485. Core functionality works with no third-party libraries beyond the standard library (BM25/storage/retrieval/pruning).149150## 8. Reference Implementation151152The complete reference implementation for this skill lives in the workspace `agent-memory/` (`hermes_lite.py` single file ~1600 lines, 79 tests, 150-memory stress test all passing). Reproduce from this spec; use the reference only to cross-check extreme details inconsistent with this appendix. After implementing, read `Token节省实测分析报告v2.md` to understand the system's token-cost model.153154## 9. Data Formats & Configuration Reference (Appendix — implement exactly)155156### 9.1 store/*.md format (Obsidian-compatible, YAML frontmatter)157158```markdown159---160id: mem_20260817_033106_ab12161category: decision162room: decisions163importance: 8164execution_verified: true165access_count: 3166last_accessed: 2026-08-17T04:00:00+00:00167source: 对话流自动沉淀168created_at: 2026-08-17T03:31:06+00:00169redacted: password,api_key170---171172正文内容(已脱敏)173```174175Fixed frontmatter field order: id, category, room, importance, execution_verified, access_count, last_accessed, source, created_at, redacted (redacted only when redaction occurred, comma-separated).176177### 9.2 index.json entry schema178179```json180{181 "id": "mem_20260817_033106_ab12",182 "content": "脱敏后的记忆文本",183 "category": "decision",184 "room": "decisions",185 "importance": 8,186 "execution_verified": false,187 "access_count": 0,188 "last_accessed": null,189 "source": null,190 "created_at": "2026-08-17T03:31:06+00:00",191 "redacted": []192}193```194195index.json top level is an array of entries; writes use atomic write (temp file + os.replace) + daily first-write auto-backup to backups/auto_index.json.YYYYMMDD.bak (keep 7). Prune archive format: `backups/prune_YYYYMMDD_HHMMSS.json` = `{"archived_at": ISO-time, "memories": [full entries...]}`.196197### 9.3 config.yaml complete reference values198199```yaml200rooms: # retention in days; display fields name/description/icon can be customized201 prefs: 365 # preferences202 decisions: 730 # decisions203 configs: 365 # configs204 projects: 545 # projects205 events: 90 # events206category_mapping:207 preference: prefs decision: decisions config: configs208 project: projects event: events discussion: events idea: events general: events209retrieval:210 default_top_k: 5 min_score: 0.5 time_decay_days: 90211 weight_importance: 0.4 weight_frequency: 0.5 weight_verified: 0.15212 boost_verified: 1.15 hot_threshold: 5 hot_boost: 1.1213pruning:214 unverified_retention_days: 7 low_frequency_days: 30215 low_frequency_threshold: 3 critical_importance: 10 default_retention_days: 90216security:217 redact_patterns: (see 9.5)218```219220> Note: weight_importance/weight_frequency/weight_verified are legacy heuristic params; the actual retrieval path uses BM25+RRF+hot boost+min_score. These weight fields are kept for compatibility but are not used by retrieval.221222### 9.4 DSH session log structure (auto_sediment parse target)223224```json225{"type":"user/message","seq":5,"time":1786932396089,"data":{"content":[{"type":"text","text":"user message"}]}}226{"type":"assistant/message","seq":6,"time":...,"data":{"message":{"role":"assistant","content":[{"type":"reasoning","text":"reasoning (skip)"},{"type":"text","text":"assistant reply"}]}}}227{"type":"tool/call","seq":7,"time":...,"data":{"name":"bash","arguments":"{\"command\":\"...\"}"}}228{"type":"tool/result","seq":8,"time":...,"data":{"message":{"source":{"kind":"tool"},"content":[{"type":"tool-result","toolCallId":"...","content":[{"type":"text","text":"tool output (skip)"}]}]}}}229```230231Extraction rules: `user/message` → `data.content[].text`; `assistant/message` → `data.message.content[]` where `type=="text"` (**skip** `reasoning`); **skip** `tool/call` and `tool/result`. Join as "用户: …\n助手: …", truncate to 8000 chars, feed to digest. `.zstd`-compressed session files are decompressed via `zstd -dc` to stdout before parsing.232233### 9.5 Redaction regexes (complete set of 5, label inference)234235```python236# pattern → redaction label237r"((?:密码|口令|passwd|password)(?:\s*(?:是|为|[::=])\s*|\s+))([^\s,,。;;]+)" # password238r"((?:api[_-]?key|apikey|access[_-]?key|密钥)(?:\s*(?:是|为|[::=])\s*|\s+))([^\s,,。;;]+)" # api_key239r"((?:token|secret|授权码)(?:\s*(?:是|为|[::=])\s*|\s+))([^\s,,。;;]+)" # token240r"(sk-[A-Za-z0-9_\-]{8,})" # api_key241r"(Bearer\s+[A-Za-z0-9_\-\.]{10,})" # token242```243244Replacement logic: when a prefix capture group (group 1) exists, keep the prefix and replace the value with `[已脱敏:label]`; with no prefix group, replace the whole match with `[已脱敏:label]`. **The decision is based on capture-group count ≥2** (prefix+value patterns) before keeping the prefix; single-group patterns (sk-xxx / Bearer xxx) must be **fully replaced** — ⚠️ an older reference version kept the whole match as the "prefix" causing plaintext leakage (`sk-abc123secret[已脱敏:api_key]`); do NOT copy that logic, follow this appendix (fixed and test-covered). Value capture uses `([^\s,,。;;]+)` (**not** `\S+`, to avoid swallowing Chinese punctuation). Execute top-to-bottom; record each label into the `redacted` field. Critical: **copula forms like "密码是 hunter2" must be redacted** — the `(?:\s*(?:是|为|[::=])\s*|\s+)` branch covers "密码:x", "密码是 x", and "密码 x".245246### 9.6 Conflict-detection regexes247248```python249pos = r"(?:决定|选择|采用|使用|选用|用)\s*(?:了|的)?\s*([A-Za-z0-9_\-.]+)" # positive assertion250neg = r"(?:放弃|不用|弃用|反对|拒绝|移除|取消|停止)\s*(?:了|的)?\s*([A-Za-z0-9_\-.]+)" # negative assertion251```252253New text's neg ∩ old memory's pos (or the reverse) → conflict, output {memory_id, entity, type, content}.254255### 9.7 recall scoring details256257- Keyword score kw_score = BM25 score (>0 is a candidate); hot memories (access_count ≥ hot_threshold) get ×hot_boost.258- Vector score vec_score = cosine (only enters fusion when ≥ threshold: local 0.30 / remote 0.35, filtered by room).259- Fusion: `rrf_merge([kw_ids, vec_ids], k=60)`.260- display_score = kw_score (when >0) else vec_score×3; drop when kw=0 and vec < min_score/3.261- Each hit updates entry.access_count+=1, last_accessed, `_save_index()`; when access_count % 10 == 0 and importance<10, importance+1.