feat: profile layer — semantic memory (consolidation step 2)

Derive a standing profile of the user from session gists and inject it into
every prompt, so identity/abstract questions ("what kind of player am I",
"what are my leaks") are answered from distilled knowledge instead of noisy
single-vector recall (which finds passages, not patterns).

- memory: profile table + get/set_profile, list_summaries
- lyra/profile.py: rebuild_profile map-reduces all gists (batch -> extract
  durable facts -> fold-merge) into one profile doc; `lyra-profile` CLI
- chat.build_messages injects "What you know about Brian" after the persona

Run after lyra-summarize (needs gists). Verified (stubbed): map-reduce, storage,
and prompt injection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 04:11:19 +00:00
parent 071522ea33
commit ecf0b852f9
4 changed files with 140 additions and 0 deletions
+47
View File
@@ -43,6 +43,15 @@ CREATE TABLE IF NOT EXISTS summaries (
last_exchange_id INTEGER NOT NULL,
created_at TEXT NOT NULL
);
-- Derived semantic memory: standing facts about the user, distilled from the
-- session gists by the consolidation pass. Single row (id='self').
CREATE TABLE IF NOT EXISTS profile (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
sessions_covered INTEGER NOT NULL,
updated_at TEXT NOT NULL
);
"""
_conn: sqlite3.Connection | None = None
@@ -290,6 +299,44 @@ def unsummarized_count(session_id: str) -> int:
return int(r["n"])
def list_summaries() -> list[Summary]:
"""Every session gist (for the profile/era consolidation passes)."""
conn = _connection()
rows = conn.execute(
"SELECT session_id, content, last_exchange_id, created_at FROM summaries "
"ORDER BY created_at ASC"
).fetchall()
return [
Summary(
session_id=r["session_id"],
content=r["content"],
last_exchange_id=r["last_exchange_id"],
created_at=r["created_at"],
)
for r in rows
]
def set_profile(content: str, sessions_covered: int, profile_id: str = "self") -> None:
"""Store/replace the derived semantic profile."""
now = datetime.now(timezone.utc).isoformat()
conn = _connection()
with conn:
conn.execute(
"INSERT INTO profile (id, content, sessions_covered, updated_at) "
"VALUES (?, ?, ?, ?) "
"ON CONFLICT(id) DO UPDATE SET content=excluded.content, "
"sessions_covered=excluded.sessions_covered, updated_at=excluded.updated_at",
(profile_id, content, sessions_covered, now),
)
def get_profile(profile_id: str = "self") -> str | None:
conn = _connection()
r = conn.execute("SELECT content FROM profile WHERE id = ?", (profile_id,)).fetchone()
return r["content"] if r else None
def recall_summaries(query: str, k: int = 3, exclude_session: str | None = None) -> list[Summary]:
"""Top-k session summaries most similar to `query` (the long-term gist tier)."""
[q_vec] = llm.embed([query])