Skip to content

Memory System

ruozhuoruoyu edited this page Jul 21, 2026 · 3 revisions

🌐 English | 中文

Memory System

Philont's memory is 5 layers, all SQLite-backed and cross-session. It is part of what lets a small model behave consistently over time: the model doesn't have to hold the relationship in its context window, because the runtime carries it.

Schema: agent-memory/src/schema.ts (current SCHEMA_VERSION = 31). Default DB path: ~/.philont/memory/memory.sqlite (override with MEMORY_DB_PATH); the parent dir is created 0700, WAL mode, with a sibling backups/.

The five layers

Layer Table(s) What it stores
0 · Raw timeline memory_raw_sessions, memory_raw_messages (+ _fts) Append-only conversation log: role (user/assistant/tool/system) + content + timestamp, with an FTS5 trigram index for full-text recall.
0.5 · Action log memory_actions Tool-call trace: tool_name, params, result, success, linked skill. The raw material reflection learns from.
1 · Notes memory_notes (+ _fts) Free-text notes / fallbacks: content, importance, access timestamps, forgotten_at. FTS5-searchable.
2 · Facts memory_facts Structured KV facts with a namespace, confidence, time semantics (fact_kind state|event, occurred_at, valid_from/until) and a per-namespace decay constant.
3 · Skills memory_skills Reusable action templates: trigger keywords, action template, use/success/failure counts, maturity grade, consecutive failures, positive|negative kind.

Facts: confidence + decay

agent-memory/src/decay.ts

Facts don't just accumulate — they decay so stale knowledge loses weight automatically. The effective score is:

score = confidence × exp(−age_days / tau)

tau is per-namespace: user.* → ∞ (never decay), project.* → 60 days, context.*/session.* → 1 day, default → 365 days. last_accessed_at is refreshed on every access (LRU-ish), and a fact can be pinned (decay_tau_days = −1) to never decay. This is how "the user moved to Berlin last year" quietly outranks "the user is in a café right now" once the café visit is days old.

Skills: maturity grades

agent-memory/src/skill_maturity.ts

A learned skill carries a maturity grade that gates whether it's trusted and callable — knowledge evolves rather than piling up unchecked. The state machine:

Grade Meaning
playbook Lesson/experience only — not callable via use_skill.
draft Written after first success; callable but flagged "verify before use."
confirmed ≥2 successes, 0 failures — injected normally.
stable ≥5 successes and failure ratio < 0.1 — highest trust.
deprecated ≥3 consecutive failures, or failure ratio > 0.3 with ≥5 successes — no longer callable; kept as a counter-example.

Promotion happens only on success and never skips a tier (draft → confirmed → stable); a failure demotes one tier (or jumps straight to deprecated when the failure threshold is hit). Skills are produced by the self-learning loop.

⚠️ What "success" counts as — and what it used to count as. Until recently, use_skill fetching a skill's body ran through the outcome recorder, so a skill retrieved twice climbed to confirmed: "confirmed" meant "fetched twice", not "worked twice" — a fabricated efficacy signal that nothing in the store could distinguish from a real one. Retrieval is now decoupled: recordUsage bumps use_count / last_used_at for ranking only, and touches neither success_count nor this state machine. Efficacy is credited solely by the reflector's attribution, which looks at whether the actions after the retrieval actually succeeded.

The climb itself is being demoted as a promotion path: under verify-then-condense, a skill is meant to be born validated — crystallized from a judge-verified success — leaving this ladder to do only what it does reliably: demotion on observed failure, plus disuse decay. That step is gated on the learning judge proving trustworthy first, so today the ladder still stands as described.

A callable recipe (a skill with a machine-checkable verification) additionally carries a revision_history: when skill self-repair rewrites a demoted recipe, SkillStore.reviseRecipe snapshots the prior version before overwriting, so a skill is versioned, not destructively replaced — and "did the rewrite actually help" becomes measurable per recipe.

Tools exposed to the model (snake_case)

agent-memory/src/tools.ts

store_fact · get_fact · list_facts · search_notes · search_skills · use_skill · recall_sessions · create_calendar_event · list_upcoming · schedule_reminder · cancel_schedule.

(Note the casing split: memory tools are snake_case; the filesystem/shell tools in agent-tools are camelCase — readFile, writeFile, glob. The honesty gate and permission classifier match the real names exactly.)

Cross-session continuity (the K0 timeline model)

The model's working context is not held across WebSocket turns. Instead, each turn re-recalls from the raw global timeline — so the agent's identity is continuous while history is brought back on demand:

  • TimelineRetriever (agent-memory/src/timeline.ts) assembles two sections under a token budget: a recency section (newest N messages) plus a keyword-recall section (FTS5 hits on the user's current message, de-duplicated against recency).
  • buildMemoryPrefix() (server/src/chat-handler.ts) prepends a condensed index of facts, skills, and notes, plus host environment and any active plan.
  • A recall trigger (agent-memory/src/recall_trigger.ts) detects when the user references a past conversation ("remember when we…", "上次讨论过…") and forces the model to call recall_sessions first, so it answers from the real record rather than guessing.

This design means context cost stays bounded regardless of how long the relationship is — the model pays only for what it recalls this turn, not for the entire history.


中文

English | 中文

记忆系统

Philont 的记忆是 5 层,全部基于 SQLite 且跨会话。它是小模型能长期保持一致表现的一部分原因:模型不必把关系装进上下文窗口,因为运行时替它携带。

Schema:agent-memory/src/schema.ts(当前 SCHEMA_VERSION = 31)。默认 DB 路径:~/.philont/memory/memory.sqlite(可用 MEMORY_DB_PATH 覆盖);父目录以 0700 创建,WAL 模式,并有同级 backups/。

五层

层 表 存什么
0 · 原始时间线 memory_raw_sessions、memory_raw_messages(+ _fts) 仅追加的对话日志:role(user/assistant/tool/system)+ 内容 + 时间戳,带 FTS5 trigram 索引用于全文召回。
0.5 · 动作日志 memory_actions 工具调用轨迹:tool_name、参数、结果、success、关联 skill。反思学习的原料。
1 · 笔记 memory_notes(+ _fts) 自由文本笔记/兜底:内容、重要性、访问时间戳、forgotten_at。可 FTS5 搜索。
2 · 事实 memory_facts 结构化 KV 事实,带 namespace、confidence、时间语义(fact_kind state|event、occurred_at、valid_from/until)以及按 namespace 的衰减常数。
3 · 技能 memory_skills 可复用动作模板:触发关键词、动作模板、使用/成功/失败计数、成熟度等级、连续失败数、positive|negative 的 kind。

事实:置信度 + 衰减

agent-memory/src/decay.ts

事实不只是累积 —— 它们会衰减,让陈旧知识自动失权。有效分数为:

score = confidence × exp(−age_days / tau)

tau 按 namespace:user.* → ∞(永不衰减),project.* → 60 天,context.*/session.* → 1 天,默认 → 365 天。last_accessed_at 每次访问刷新(类 LRU),且事实可被钉住(decay_tau_days = −1)永不衰减。这就是为什么一旦「用户刚才在咖啡馆」过了几天,「用户去年搬到柏林」会悄悄盖过它。

技能:成熟度等级

agent-memory/src/skill_maturity.ts

学到的 skill 带一个成熟度等级,决定它是否被信任、可否调用 —— 知识是演化而非无序堆积。状态机:

等级 含义
playbook 仅教训/经验 —— 不能通过 use_skill 调用。
draft 首次成功后写下;可调用但标注「用前先验证」。
confirmed ≥2 次成功、0 失败 —— 正常注入。
stable ≥5 次成功且失败率 < 0.1 —— 最高信任。
deprecated ≥3 次连续失败,或 ≥5 次成功且失败率 > 0.3 —— 不再可调用;作为反例保留。

晋升只在成功时发生且不跳级(draft → confirmed → stable);失败降一级(或在触及失败阈值时直接跳到 deprecated)。技能由自学习回路产生。

⚠️ 「成功」算什么 —— 以及它过去算什么。 直到不久前,use_skill「把技能正文取出来」这个动作也会走结果记录器,于是一个被取过两次的技能就爬到了 confirmed:「confirmed」的真实含义是「被取过两次」,而不是「管用过两次」 —— 一个伪造的有效性信号,而库里没有任何东西能把它和真信号区分开。现在取用已解耦:recordUsage 只更新 use_count / last_used_at 供排序,既不碰 success_count 也不碰这台状态机。有效性只由 reflector 的归因记账 —— 它看的是「取用之后的动作到底成没成」。

这架梯子本身作为「晋升通道」正在被降格:在先验证再凝结的框架下,技能应当生下来就是被验证的 —— 从裁判确认的成功里结晶出来 —— 而梯子只保留它做得可靠的那部分:观察到失败时降级 + 闲置衰减。这一步卡在「学习裁判先被证明可信」之后,所以今天梯子仍如上所述工作。

一个 callable recipe(带机器可校验 verification 的技能)额外携带一个 revision_history:当技能自修复改写一个被降级的 recipe 时,SkillStore.reviseRecipe 会在覆盖前把旧版本存快照,所以技能是有版本的,不是破坏性替换——"这次改写到底有没有用"因此可以按 recipe 度量。

暴露给模型的工具(snake_case)

agent-memory/src/tools.ts

store_fact · get_fact · list_facts · search_notes · search_skills · use_skill · recall_sessions · create_calendar_event · list_upcoming · schedule_reminder · cancel_schedule。

(注意大小写分裂:记忆工具是 snake_case;agent-tools 里的文件/shell 工具是 camelCase —— readFile、writeFile、glob。诚实门与权限分类器都按真名精确匹配。)

跨会话连续性(K0 时间线模型)

模型的工作上下文不跨 WebSocket turn 保留。相反,每 turn 从原始全局时间线重新召回 —— 因此 agent 身份连续,而历史按需取回:

  • TimelineRetriever(agent-memory/src/timeline.ts)在 token 预算下组装两段:最近段(最新 N 条)加关键词召回段(对用户当前消息的 FTS5 命中,与最近段去重)。
  • buildMemoryPrefix()(server/src/chat-handler.ts)在前面拼上 facts/skills/notes 的浓缩索引,以及主机环境和任何活跃 plan。
  • 一个召回触发器(agent-memory/src/recall_trigger.ts)检测用户提及过往对话(「记得我们…」「上次讨论过…」)并强制模型先调 recall_sessions,从而依据真实记录作答而非猜测。

这一设计意味着无论关系多长,上下文成本都有界 —— 模型只为本 turn 召回的内容付费,而非整段历史。

Clone this wiki locally