@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).

Packages

Package details

extension

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