This repository has been archived on 2026-07-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
mission-control/routers/memory.py
T
Hitonabi 8539999627 fix+docs: Bug-Fixes, vollstaendige Dokumentation v8
Bug-Fixes:
- hermes_agent.py: asyncio.get_event_loop() → get_running_loop() (Python 3.10+)
- routers/hermes.py: Thread-Lock fuer Whisper-Init, HERMES_WINDOWS_USER Import,
  Piper stderr logging, __import__ Anti-Pattern entfernt
- routers/memory.py: SQLite WAL-Mode, Kategorie-Enum-Validierung (user/instruction/stable/versioned/ephemeral)
- HermesPanel.svelte: findLast() → reverse().find() (Browser-Kompatibilitaet)
- ConnectPanel.svelte: Hardcoded Username durch Platzhalter ersetzt

Docs:
- CLAUDE.md: komplett aktualisiert (v7+v8, Hermes, Memory, alle Env-Vars)
- ROADMAP.md: v7+v8 als erledigt, naechste Features (v8.1-v9.1)
- README.md: komplett neu geschrieben (Agentic OS Konzept, alle Features)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 14:48:22 +02:00

165 lines
5.1 KiB
Python

"""
Memory Layer fuer Mission Control.
Persistentes Gedaechtnis fuer lokale LLM-Agenten (Cline, Claude Code, OpenCode).
SQLite aus stdlib — kein Vektor-Overhead, keine neue Abhaengigkeit.
Alle MCP-Tools teilen denselben Speicher via mcp_memory.py-Wrapper.
"""
import sqlite3
import uuid
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from auth import auth
from config import MEMORY_DB
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
_db_conn: Optional[sqlite3.Connection] = None
def _db() -> sqlite3.Connection:
global _db_conn
if _db_conn is None:
MEMORY_DB.parent.mkdir(parents=True, exist_ok=True)
_db_conn = sqlite3.connect(str(MEMORY_DB), check_same_thread=False)
_db_conn.row_factory = sqlite3.Row
_db_conn.execute("PRAGMA journal_mode=WAL") # sicherer bei concurrent FastAPI-Threads
_db_conn.execute("""
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'stable',
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
# Temporaere Eintraege nach 7 Tagen automatisch loeschen
_db_conn.execute(
"DELETE FROM memories WHERE category = 'ephemeral'"
" AND datetime(created_at) < datetime('now', '-7 days')"
)
_db_conn.commit()
return _db_conn
class _MemCategory(str, Enum):
user = "user"
instruction = "instruction"
stable = "stable"
versioned = "versioned"
ephemeral = "ephemeral"
class _MemIn(BaseModel):
content: str
category: _MemCategory = _MemCategory.stable
source: str = "manual"
class _MemUp(BaseModel):
content: Optional[str] = None
category: Optional[_MemCategory] = None
def _row(r: sqlite3.Row) -> dict:
return dict(r)
# Export muss vor /{mid} stehen, sonst wuerde FastAPI "export" als ID behandeln
@router.get("/memory/export")
def export_memories():
"""Alle Eintraege als strukturierter Plain-Text — direkt als System-Prompt-Kontext nutzbar."""
db = _db()
rows = db.execute(
"SELECT * FROM memories ORDER BY category, updated_at DESC"
).fetchall()
cat_names = {
"stable": "Stabile Fakten",
"versioned": "Versionierte Infos",
"ephemeral": "Temporaerer Kontext",
}
lines = ["# Mission Control — Gedaechtnis\n"]
current = ""
for r in rows:
cat = cat_names.get(r["category"], r["category"])
if cat != current:
lines.append(f"\n## {cat}\n")
current = cat
lines.append(
f"- {r['content']} _(Quelle: {r['source']}, {r['updated_at'][:10]})_"
)
return {"text": "\n".join(lines), "count": len(rows)}
@router.get("/memory")
def list_memories(q: str = "", category: str = ""):
db = _db()
sql = "SELECT * FROM memories"
params: list = []
conds: list = []
if q:
conds.append("content LIKE ?")
params.append(f"%{q}%")
if category:
conds.append("category = ?")
params.append(category)
if conds:
sql += " WHERE " + " AND ".join(conds)
sql += " ORDER BY created_at DESC"
return [_row(r) for r in db.execute(sql, params).fetchall()]
@router.post("/memory", status_code=201)
def add_memory(body: _MemIn):
now = datetime.now(timezone.utc).isoformat()
mid = str(uuid.uuid4())
db = _db()
db.execute(
"INSERT INTO memories (id, content, category, source, created_at, updated_at)"
" VALUES (?,?,?,?,?,?)",
(mid, body.content.strip(), body.category, body.source, now, now),
)
db.commit()
return {
"id": mid, "content": body.content.strip(),
"category": body.category, "source": body.source,
"created_at": now, "updated_at": now,
}
@router.put("/memory/{mid}")
def update_memory(mid: str, body: _MemUp):
db = _db()
row = db.execute("SELECT * FROM memories WHERE id = ?", (mid,)).fetchone()
if not row:
raise HTTPException(404, "Eintrag nicht gefunden")
now = datetime.now(timezone.utc).isoformat()
content = body.content.strip() if body.content is not None else row["content"]
category = body.category if body.category is not None else row["category"]
db.execute(
"UPDATE memories SET content=?, category=?, updated_at=? WHERE id=?",
(content, category, now, mid),
)
db.commit()
return {
"id": mid, "content": content, "category": category,
"source": row["source"], "created_at": row["created_at"], "updated_at": now,
}
@router.delete("/memory/{mid}")
def delete_memory(mid: str):
db = _db()
if not db.execute("SELECT id FROM memories WHERE id = ?", (mid,)).fetchone():
raise HTTPException(404, "Eintrag nicht gefunden")
db.execute("DELETE FROM memories WHERE id = ?", (mid,))
db.commit()
return {"ok": True}