@qltk/pi-mnemo
Persistent memory for the pi coding agent — auto recall/extract/dream, file-based (MEMORY.md index + topic files), global + project dual scope. Port of mnemo (opencode).
Package details
Install @qltk/pi-mnemo from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@qltk/pi-mnemo- Package
@qltk/pi-mnemo- Version
0.8.2- Published
- Oct 28, 2026
- Downloads
- 2,255/mo · 1,502/wk
- Author
- lby3523
- License
- MIT
- Types
- extension
- Size
- 272.3 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-mnemo
pi coding agent 的持久记忆扩展。mnemo(opencode 版)的 pi 移植:文件式、四类型、MEMORY.md 索引 + topic 文件、三段全自动。
纯 TypeScript 扩展,无 native 依赖、无 Docker、不用 RAG/向量检索。相关性靠 LLM-judge(注入索引 → agent 扫标题 → 按需读 topic)。
工作机制(三段)
| 段 | 触发 | 机制 |
|---|---|---|
| Recall | 每次用户提交 prompt(before_agent_start) |
把 inject.md 指令 + global/project 两个 MEMORY.md 索引追加进 system prompt |
| Extract | 会话安定(agent_settled),节流后 |
spawn 分离的无头 worker pi -p --no-session,自读会话 JSONL、抽取新记忆写 topic 文件 |
| Dream | 会话安定 + 24h 节流 + 夜间窗口 | 无头 worker 合并/去重/软删(deprecated_by)/修正漂移/清理索引 |
另有 citation 闭环:主 agent 用 read 工具读过某 topic 文件 → 自动 bump 该 topic 的 usage_count / last_used(不依赖模型自觉);dream 据此修剪长期未引用的过期 topic。
主 agent 只在用户显式说"记住"时直接写;其余抽取由后台 worker 负责。
安装
# npm(推荐;pi.dev gallery 自动收录带 pi-package 关键字的包)
pi install npm:@qltk/pi-mnemo
# git 源
pi install git:gitee.com/liubola/pi-mnemo
# 本地开发(放自动发现目录,支持 /reload 热重载)
ln -s /path/to/pi-mnemo ~/.pi/agent/extensions/pi-mnemo
使用
装好后全自动。命令:
/mnemo— 状态(memory root、slug、topic 数、上次 dream 时间)/mnemo extract— 手动触发本会话抽取/mnemo drain— 手动补提「本会话目录里欠提的其他会话」(旁路断路器;正常情况每 settle 自动补 1 个)/mnemo drain-all— 跨项目桶一次性补提(最多 8 个;slug 只认会话 header 的 cwd,拿不到就跳过)/mnemo dream— 手动触发整理/mnemo refresh— 丢弃本会话记忆快照,下轮注入最新索引(代价:一次全量 prefill)
对模型说"记住 …"会立即写入(见 inject.md 指令)。
extract 覆盖与自愈(0.7.0):欠提与否不看待办队列,只看「会话 jsonl 行数 > 游标」这个派生态。每次 agent_settled 顺带扫本项目的会话目录,补提最多 extractDrainPerSettle 个欠提的其他会话(每个会话的 slug 从其 jsonl header 的 cwd 派生);session_shutdown(pi-web 空闲即 emit,不是进程退出)对该会话做一次尾提,旁路两道节流——否则最后一轮只有 2 行的尾巴会永久丢失。worker 连败 3 次开断路器:自动 extract 暂停 30min(指数退避,上限 6h)并落一条带死因线索(.worker.log 末行)+ 当前 extractModel 的醒目告警;/mnemo extract 不受限,即天然探针。
dream backlog(0.7.0):超过 dreamBacklogDays(默认 2 天)没 dream 的项目桶,会在别的会话 settle 时被顺带补做(每次 dreamBacklogPerIdle 个,最久没动的先补、从未 dream 过的排最前),仍过 24h 节流与夜间窗口两道门。此前只有「晚上恰好有会话 settle」的桶会被合并,冷桶实测 7 周不整理。补做冷桶时默认旁路夜窗(dreamBacklogIgnoreNightWindow,0.8.2 起默认 true——多数装机是「晚上关机」型,夜窗内根本没有 settle,不旁路 = 冷桶永不自愈);当前会话自己的 dream 仍守窗口。想改回守窗就把该键设 false。
索引预算(0.7.0):索引行是指针——读侧机械截断到 200 字符(](topic.md) 指针永不切掉),age 注解计入预算,索引超出 200 条 / 25KB 时折叠最早的条目并在 prompt 里自报(旧版是截文件尾部 = 丢最新条目,且注解在预算外,实际注入 ≈32KB)。超长索引行是把 topic 正文搬进索引的产物,它同时导致目录丢失与 citation 信号饥饿(模型不再 open topic)。
并发让路与快照域(0.8.0):dream 会在 spawn 前给自己的写入域(global 顶层 + projects/<slug>/)拍一份 pre-image,校验不合格就按同一域回滚——过去快照是整树的(使用机实测单次 369 个 .md),于是「dream 在 A 桶、extract 在 B 桶」并行时,B 刚提交的记忆会被 A 的回滚抹掉而游标不回退 = 永久失踪。现在两件事一起修:①让路表加了跨桶那一列(extract.* 遇任何在飞 dream 让路,dream.auto 遇任何在飞 extract 让路,两个 manual 仍是手动阀但会打警告);②快照/回滚的域 == 写入域 == 校验域。后果是夜窗里 dream 在飞的几分钟内,自动 extract 会被推迟(游标是派生态,下一轮 drain/尾提补上,不丢数据);/mnemo status 的 workers in flight 行能看出当前谁在飞。dream 若写了写入域之外,只报告不回滚(日志 + 审计字段 outsideDomainWrites),因为域外那份很可能正是别的桶刚提交的记忆。
.dream-backup 只有一份槽位:上一轮 dream 的宿主先退(pi -p 常见)时,遗留的未校验现场由下个进程首次 dream 前补做校验(护栏:本进程无在飞 + 锁无活进程 + 快照陈旧 ≥5min)。若新 dream 抢在护栏放行之前起跑,会覆盖那份现场——会留一行 ⚠ dream 将覆盖一份未校验快照(age Ns) 并在审计里带 overwroteUnvalidatedSec,但不做多份保留(这是定案,不是缺陷)。
索引双向对账(0.8.0):MEMORY.md 与 topic 文件的两种失配都由插件机械修复,不靠模型自觉——①文件在、索引没行 → 补行;②索引有行、但目标文件不存在或已软删(deprecated_by)→ 移出行(死行会占索引预算、还会骗模型去读不存在的文件)。两条都在 extract 提交后与 dream 回滚后跑,日志分别是 index reconciled — N 行补进 … / … N 行移出 …。追加行跟随文件自身的换行风格(CRLF 库不会被插进 LF 行)。
一次性会话也会补历史洞(0.8.0):过去短会话里自有的 extract 占住同桶 lease,同目录的 drain 候选必然全部让路(实测 extract drain — spawned 0 skipped 77 candidates 96),历史洞只能靠长会话逐 settle 吃。现在本会话一提完(且不再需要 chain、桶空出来)就当场补一次 drain;让路语义不变(同桶永不两个 worker 写同一份索引)。drain 的跳过原因也分桶计数了:(minNew=… circuit=… sameBucket=… noSlug=… gate=…)。
记忆快照语义:每个会话首条消息时冻结一份记忆索引快照,会话内字节固定——为了让本地推理(llama.cpp 等 KV 前缀缓存)不为后台记忆更新付全量重算的代价。会话中途后台 extract/dream 写入的新记忆,下一会话可见;急需时 /mnemo refresh 主动换新(本会话付一次全量 prefill)。
Windows worker 监管与跨进程语义(0.8.1)
- kill 真的会杀:win32 用
taskkill /PID <pid> /T /F(只按 PID,绝不/IM——同名多实例会连坐误杀);POSIX 杀进程组、失败回退单杀。旧版负 pid 在 Windows 恒抛 ESRCH 被吞,kill 完全空转(实测被标记的 worker 活着跑完 19 分钟还正常 code 0)。 - 判活是多源复合:stderr 日志 mtime + 心跳文件(worker 按 prompt 纪律每批工作
touch ~/.pi/agent/mnemo/.worker-heartbeat)+ 内存树最新 .md mtime + stdout 取证文件大小;Windows CPU 走tasklist /V解析。旧版只看.worker.log的 mtime——那是失败取证文件,只在 worker 出事时才写,于是 Windows 上 idle 永不归零、任何跑过 5 分钟的 worker 都被误标。 - dream 的 idle 窗独立放宽:
dreamWorkerIdleTimeoutMs默认 15 分钟(extract 仍 5 分钟)——dream 通读 global 几十个文件再动笔是常态,不是挂死。 - 被杀/非零退出的 dream 一律强制回滚(审计
verdict=worker-failed, rolledBack=true):validate 只查索引形状,不查语义完整;信一个被我们杀掉的 worker 的中间产物,不如回到快照重来。 - 跨进程是 advisory 让路(不是锁):extract(含
/mnemo extract)起跑前看.dream.lock有没有被活进程持有;dream 起跑前扫.extract-inflight/登记册(真实子进程 pid,退出注销、死 pid 自愈清扫)。两方向都只是让路——错过的代价是推迟一轮,不涉及 shared-store 互斥(ADR-0001 的边界不变)。不变式:每个 dream 的写入域都含 global,dream↔dream 不撞只靠.dream.lock全局串行。 - dream_log.md 有滚动:超 64KB 截到最近 ~32KB(字节级,从条目边界起),不再无限长。
使用侧口径(写记忆的习惯)
索引行是指针,不是摘要副本。硬口径:
- 一条索引行 ≤150 字符。读侧的机械截断预算是 200 字符含 age 注解(
[N days old — verify against current code]占 44–46 字符),所以超过 1 天的条目实际只有 ~156 字符的行本体预算 —— 190 字符的写法会被切掉后半截,不是「200 以内都安全」。 - 细节一律进 topic 正文。
- [名字](topic.md) — 钩子的指针在最前面,截断永不切掉链接(这条有测试钉住),但被切的正是后半截描述,所以把最关键的一眼结论放在钩子开头。 - 索引总预算 200 条 / 25KB;超了会折叠最早的条目并在 prompt 里自报(不是静默丢尾部)。
- 正文文件的换行风格(CRLF / LF)由写它的工具决定,插件不规范化;插件自己追加索引行时跟随该文件已有的风格。注意:worker 用它自己的写工具改
MEMORY.md时可能把整份索引归一成 LF——这不影响插件解析,只影响宿主侧按原串定位的 edit 锚点。 - 不必手工核对索引一致性:两种失配(缺行 / 死行)都会被自动修(见上)。
记忆落盘位置
~/.pi/agent/mnemo/ ← memoryRoot
├── MEMORY.md global 索引(user 类型,跨项目)
├── <topic>.md user 类型 topic
├── projects/<slug>/ per-project(slug 由项目路径末段生成,保留 CJK:`旅游` → `projects/旅游/`;仅符号/空目录名落 default 桶)
│ ├── MEMORY.md project 索引
│ └── <topic>.md project/feedback/reference 类型
├── .plugin.log 调试日志
├── .state.json extract/dream 节流游标
└── .dream.lock dream 互斥锁
双跑提示(mnemo / pi-mnemo):两边记忆格式完全兼容(frontmatter + MEMORY.md 索引),可整目录迁移复制;不建议双端同时指向同一 store——state/索引的跨进程写保护未做(见 docs/adr/0001)。
四类型 → scope
| type | scope | 记什么 |
|---|---|---|
| user | global | 用户角色、跨项目偏好 |
| feedback | project | 协作纠正(规则 + 原因 + 适用范围) |
| project | project | 代码/git 推导不出的背景、决策、约束 |
| reference | project | 外部系统链接 |
配置
推荐用配置文件 ~/.pi/agent/mnemo.json(或 $PI_MNEMO_CONFIG 指定路径),优先级 文件 > env > 默认。改完重启 pi 生效(pi-web:systemctl --user restart pi-web)。
{
"extractModel": "zen/big-pickle", // 抽取 worker(免费档即可)
"dreamModel": "zai-coding-cn/glm-5.3-flash", // 整理 worker(建议大窗口)
"dreamWindow": "18:30-08:30", // dream 夜间窗口,"" = 全天
"disabled": false
}
全部字段(表由 npm run gen:config 从 CONFIG_FIELDS 生成,勿手改):
| file key | env | 默认 | 说明 |
|---|---|---|---|
memoryRoot |
PI_MNEMO_ROOT |
~/.pi/agent/mnemo | 记忆根目录(global 索引 + topic + projects//) |
promptsDir |
PI_MNEMO_PROMPTS_DIR |
/prompts | prompt 模板目录(开发期一般不动) |
extractModel |
PI_MNEMO_EXTRACT_MODEL |
(pi 默认模型) | extract worker 模型(provider/id;空 = pi 当前默认) |
dreamModel |
PI_MNEMO_DREAM_MODEL |
同 extract | dream worker 模型(建议大窗口) |
workerTools |
PI_MNEMO_WORKER_TOOLS |
read,grep,find,edit,write,bash | worker 工具白名单(必须是 pi 内置名:find 不是 glob) |
workerExtensions |
PI_MNEMO_WORKER_EXTENSIONS |
(空) | worker 额外加载的扩展(逗号分隔:路径 / npm: / git: 源)——--no-extensions 仍挡自动发现,此处仅显式放行(如网关类 provider) |
dreamIntervalMs |
PI_MNEMO_DREAM_INTERVAL_MS |
86400000 | dream 节流(per-slug,24h) |
dreamWindow |
PI_MNEMO_DREAM_WINDOW_START/END |
18:30-08:30 | dream 夜间窗口(文件 "18:30-08:30";空 = 全天) |
extractMinNewMessages |
PI_MNEMO_EXTRACT_MIN_NEW_MESSAGES |
5 | 触发 extract 的最小新增 JSONL 行数 |
extractMinIntervalMs |
PI_MNEMO_EXTRACT_MIN_INTERVAL_MS |
1800000 | 同一会话两次 extract 最小间隔 |
extractDrainPerSettle |
PI_MNEMO_EXTRACT_DRAIN_PER_SETTLE |
1 | extract drain:每次 settle 最多补提几个同目录欠提会话(0 = 关) |
extractDrainMaxConcurrent |
PI_MNEMO_EXTRACT_DRAIN_MAX_CONCURRENT |
2 | 同时在飞的 extract worker 上限(drain 不得越过;本地模型上控算力) |
extractCircuitOpenAfter |
PI_MNEMO_EXTRACT_CIRCUIT_OPEN_AFTER |
3 | 连败几次开 extract 断路器(0 = 永不) |
extractCircuitCooloffMs |
PI_MNEMO_EXTRACT_CIRCUIT_COOL_OFF_MS |
1800000 | extract 断路器首次 cooloff(逐轮翻倍,上限 6h 写死) |
extractRetryBackoffMs |
PI_MNEMO_EXTRACT_RETRY_BACKOFF_MS |
300000 | 刚失败的会话在 drain 里的重试地板(auto 路径不受影响) |
dreamRetryBackoffMs |
PI_MNEMO_DREAM_RETRY_BACKOFF_MS |
1800000 | dream 失败后 per-slug 重试地板(逐 slug 翻倍,上限 12h 写死) |
dreamBacklogIgnoreNightWindow |
PI_MNEMO_DREAM_BACKLOG_IGNORE_NIGHT_WINDOW |
true | true = 冷桶补做(backlog)旁路夜间窗口(当前 slug 仍守窗口)。0.8.2 起默认 true:多数装机是「晚上关机」型,夜窗内根本没有 settle,不旁路 = 冷桶永不自愈(实测 7 周) |
indexMaxLineChars |
PI_MNEMO_INDEX_MAX_LINE_CHARS |
200 | 索引单行读侧硬截断上限(](topic.md) 指针永不切;0 = 不截) |
indexMaxLines |
PI_MNEMO_INDEX_MAX_LINES |
200 | 索引注入条目上限(超出则折叠最早的并自报) |
indexMaxBytes |
PI_MNEMO_INDEX_MAX_BYTES |
25000 | 索引注入字节硬顶(age 注解计入;超出折叠而非静默丢尾部) |
dreamBacklogDays |
PI_MNEMO_DREAM_BACKLOG_DAYS |
2 | dream backlog:超过 N 天未 dream 的 slug 自动补做 |
dreamBacklogPerIdle |
PI_MNEMO_DREAM_BACKLOG_PER_IDLE |
1 | dream backlog:每次 settle 补几个 overdue slug(0 = 关) |
pruneAgeDays |
PI_MNEMO_PRUNE_AGE_DAYS |
30 | dream 修剪硬阈值(天数 + 低使用) |
coldStartDays |
PI_MNEMO_COLD_START_DAYS |
14 | usage 统计冷启动(此前不按 usage 修剪) |
topicSoftMaxKB |
PI_MNEMO_TOPIC_SOFT_MAX_KB |
8 | topic 文件软上限(KB) |
logMaxBytes |
PI_MNEMO_LOG_MAX_BYTES |
1048576 | .plugin.log / .worker.log 滚动阈值 |
workerIdleTimeoutMs |
PI_MNEMO_WORKER_IDLE_TIMEOUT_MS |
300000 | worker 无活跃判定窗(防挂死不防慢) |
workerHardCapMs |
PI_MNEMO_WORKER_HARD_CAP_MS |
3600000 | worker 总时长硬顶 |
dreamWorkerIdleTimeoutMs |
PI_MNEMO_DREAM_WORKER_IDLE_TIMEOUT_MS |
900000 | dream worker 无活跃判定窗(读多写少是常态,默认比 extract 宽 3 倍;使用侧三轮复核 ②) |
disabled |
PI_MNEMO_DISABLED |
0 | true / 1 = 关后台(只留 recall + 手动命令) |
另有两个 env-only(不在 CONFIG_FIELDS 内):PI_MNEMO_CONFIG(配置文件路径,默认 ~/.pi/agent/mnemo.json)、PI_MNEMO_PI_BIN(spawn worker 用的 pi 可执行文件,默认 pi)。配置文件里的未知 key 启动时会在 stderr 警告并忽略;数值字段写坏会警告并回退默认值。
与 mnemo(opencode 版)的差异
| mnemo | pi-mnemo | |
|---|---|---|
| Recall 钩子 | experimental.chat.system.transform |
before_agent_start(每轮注入最新索引) |
| Extract 执行 | opencode server fork session + promptAsync |
分离进程 pi -p --no-session,自读会话 JSONL |
| 会话泄漏防护 | forkSessions Map + session.delete |
不需要(--no-session 不落盘,进程退出即清理) |
| Citation 观测 | 拉取 server transcript | 直接扫会话 JSONL 文件的 toolCall 行 |
| Extract 游标 | message id | JSONL 行号(天然增量和断点) |
| 额外钩子 | — | session_shutdown 兜底 flush |
License
MIT