coding-agents
vectorize-io/hindsightHindsight:会学习的 Agent 长期记忆系统,自动召回/保存、知识页、深度反思与按仓库隔离的记忆银行。
把 omp(oh-my-pi)的项目级记忆接入 DeepSeek Harness:DSH 注入的是 omp 读的那同一份 memory_summary.md 与 learned.md 窗口,其 learn 工具写回的也是同一个 learned.md。默认保守——绝不删除,撞到 omp 的 100 条封顶时拒绝淘汰,写入逐字节保真且写前 CAS 复核,也不会替 omp 静默新建记忆目录。已做黑盒对跑:同一句提示词分别喂给 omp 无头模式与 DSH 一次性任务,答案一致;无记忆的对照组两端都明确回答没有。
把 omp(oh-my-pi coding agent)的项目级记忆接入 DeepSeek Harness,让两个 agent 在同一个项目里 共用同一份教训沉淀。
memory_summary.md + learned.md 的一个窗口learn 工具,把新教训写回同一个 learned.mdlearn / autolearn 完全不受影响,两边看到的内容一致这个插件会写你的文件系统,而且那份文件是另一个 agent 的。所以它的默认行为是保守的:
| 承诺 | 实现方式 | 可验证性 |
|---|---|---|
| 绝不删除任何东西 | 插件源码里不存在 rm / unlink / truncate / rmdir 调用,由测试静态扫描强制 | V19 |
| 唯一可写目标 | join(root, <项目键>, "learned.md") 一个文件 | V24 |
| 不碰 omp 的其它产物 | MEMORY.md / memory_summary.md / raw_memories.md / rollout_summaries/ / skills/ / config.yml 一律只读或不碰 | V19 / V24 |
| 封顶淘汰默认拒绝 | omp 的教训上限是 100 条,写入会淘汰最旧的。DSH 默认 evictionPolicy: "refuse":会淘汰就拒绝写入并如实回报,文件零变化 | V21 |
| 字节保真 | 不改 BOM、不归一化换行、不整文件 trim,只在第一个 bullet 段前插入一行 | V20 |
| 并发写不覆盖 | 写前 CAS 指纹复核(size + mtime + sha256);检出并发改动就重读合并,连续冲突就放弃写入并回报 | V22 |
| 只读模式零写 | writeEnabled: false ⇒ 对记忆目录零文件系统写操作 | V23 |
| 一键刹车 | enabled: false ⇒ 插件不碰文件系统、不注册任何东西 | 配置 |
| 真实目录零污染 | 测试全程以真实记忆目录做只读校验,比对 (size, mtimeMs, sha256) | V24 |
不残留 {{...}} 占位符 | 记忆里真实存在 {{var}};DSH 渲染时插值失败会抛异常、整轮请求报废 | integration.test.ts(调 DSH 真实的 renderPrompt) |
| 不盲写 | 指纹读失败(Windows EPERM/EBUSY)不等于「文件没变」;读不到就是冲突,绝不覆盖 | V26 |
| 不认撕裂文件 | 0 字节的 learned.md(omp 用 Bun.write 中途截断)一律判为冲突,不写 | V25 |
| 不改坏字节 | 文件不是合法 UTF-8 ⇒ 拒绝写回(否则非法字节会永久变成乱码) | V32 |
| 不注入指令 | 读侧过滤指令性内容并如实上报拦截条数;写侧拒收投毒与凭据形状 | V28 / V29 / V31 / V33 |
| 不静默建目录 | 项目记忆目录不存在时拒绝写入并说明原因,不替 omp 创建 | 写路径闸门 |
为什么默认拒绝淘汰? omp 的教训上限是 100 条,超了就静默淘汰最旧的。在真实项目里,DSH 每写一条都会 加速这个淘汰,而 omp 自己毫不知情。默认
refuse把这个决定权交回给人: 要不要用历史沉淀换一条新教训,你说了算。 确实需要与 omp 完全一致时,配置evictionPolicy: "omp-parity"即可(仍会如实回报淘汰条数)。
这个上限由 omp 强制。本插件只能控制自己这一侧(默认 refuse ⇒ DSH 绝不触发淘汰),
omp 自己继续写就会淘汰掉它最早的沉淀。
本插件对此的处理:
learn 撞顶时拒绝写入,并报出真实总数与剩余额度(例:「当前已有 100 条教训…距上限还差 0 条」)。Project: 行标明这是哪个项目的记忆。⚠️ 注意:这条风险不因装了这个插件而消失。插件只能保证「DSH 不加速它」。 要真正控制它,得改 omp 侧的上限(那是另一个话题,本插件不碰 omp 的配置)。
| 组件 | 要求 | 备注 |
|---|---|---|
| DeepSeek Harness | >=0.2.1-rc.1 <0.3.0-0 | peer dependency 声明了显式预发布分支,避免 npm/semver 静默排除预发布版 |
| Node.js | >=22.6.0 | |
| omp (oh-my-pi) | v18.12.2 及以后 | 脱敏与写入语义对齐 v18.12.2;v18.12.3 的相关上游文件逐字节相同,行为等价 |
| 平台 | Windows / Linux / macOS | 平台相关的路径归一化在测试中显式覆盖 |
DSH 的官方包一律声明在 peerDependencies(不作为生产依赖打包),插件本身只引入
@deepseek-ai/schemastery 一个运行时依赖。
dsh plugin --profile web add https://github.com/meisijiya/omp-memory-dsh/releases/download/v0.2.0/omp-memory-dsh-0.2.0.tgz
prepare 自动构建)dsh plugin --profile web add github:meisijiya/omp-memory-dsh
本包是一个 DSH bundle:package.json 里声明了 dsh.bundle.patch,
所以 dsh plugin add 装上之后会自动生效,不需要额外写 patch。
如果你要手动挂载,把插件加进 profile 的 package.json 依赖,然后在
cordis.patch.yml 里写包名(不是绝对路径):
- insert:
- id: omp-memory
name: 'omp-memory-dsh'
config:
enabled: true
evictionPolicy: 'refuse'
🔴 为什么是包名而不是绝对路径? DSH 的 loader 用
import(name)加载插件。Node 的 ESM 在 Windows 上不接受裸绝对路径 (会报ERR_UNSUPPORTED_ESM_URL_SCHEME),而且绝对路径换台机器就断。 包名由 profile 的node_modules解析,是唯一可分发的写法。 (本仓库里cordis.patch.yml就是这个文件本身。)
装完需要重启 DSH(插件在进程启动时装载)。
本仓库根目录带一份 format-1 插件市场索引 index.json。
在「插件 → 发现 → 索引源」里加上它的地址(https://raw.githubusercontent.com/meisijiya/omp-memory-dsh/main/index.json),
omp-memory-dsh 就会出现在「发现」页,可点安装、可人工启用 / 停用 / 卸载。
索引格式由宿主校验器定义,规则多、易踩,所以本仓库带了一个闸门:
npm run verify:market-index
完整说明(三个插件界面的区别、索引源怎么加、启停卸载各改什么、以后发 npm 要改哪几处)见
docs/plugin-market.md。
所有配置项都有默认值,不配置也能跑。
| 配置项 | 类型 | 默认 | 说明 |
|---|---|---|---|
enabled | boolean | true | 总开关。false ⇒ 不碰文件系统、不注册任何东西 |
inject | boolean | true | 是否向 system prompt 注入记忆 |
root | string | "" ⇒ ~/.omp/agent/memories | 记忆根目录。Linux 上 omp 用 $XDG_STATE_HOME/omp/memories,此时必须显式指定 |
writeEnabled | boolean | true | 写闸门。false ⇒ 零写操作,只保留只读注入 |
evictionPolicy | "refuse" | "omp-parity" | "refuse" | 触发 100 条封顶时是否允许淘汰最旧的教训 |
sectionOrder | number | 950 | 注入 section 的排序位(落在 FILE_REFERENCE 900 与 TOOL_BASH 1000 之间) |
lockTimeoutMs | number | 2000 | 写锁等待超时。超时报错,绝不静默阻塞 |
requireApproval | boolean | false | 是否让 learn 走审批通道。只有在部署里确实装了审批服务时才开,否则 ask 会降级为拒绝,learn 全部失败 |
ledger | boolean | true | 是否写 sidecar 溯源账本(写在 omp 记忆目录之外:~/.dsh/omp-memory-dsh/ledger/) |
maxPageLines | number | 200 | memory_read 单次最多返回的行数 |
<root>/<项目键>/memory_summary.md 与 learned.md。learnedBudget = 5000 - ceil(摘要字符数 / 4)。learned.md 只读窗口,不整读:读文件头部(60%)与尾部(40%),中间省略。interpolate: false 的 section 里,外面包着 <omp-memory-data> 标签,
并附上 omp 原版的 6 条 Rules。项目键在会话开始时冻结。 DSH 的工作目录在会话中可变,但项目键不变 —— 否则
learn会写到另一个项目的目录里去。
learn 工具的完整闸门顺序(任何一步不过,都不写文件):
内容检查(拒绝指令式内容)
→ 格式与长度归一(脱敏 + 中和注入 + 上限 2000/400 字符)
→ writeEnabled 闸门
→ 目录存在性检查(绝不静默新建目录)
→ 文件锁(withFileLock,超时即报错)
→ 撕裂守卫(无尾换行 ⇒ 复读一次比对)
→ 计算新内容(去重 + 插入第一个 bullet 段 + 封顶处理)
→ 封顶闸门(refuse 模式下会淘汰就拒绝)
→ 写前 CAS 指纹复核(size + mtime + sha256)
→ 原子写(writeFileAtomic)
→ 写后校验
→ 写 sidecar 溯源账本
目录必须先存在。 如果这个项目在 omp 那边还没有记忆目录,
learn会拒绝写入并说明原因, 而不是替 omp 建一个 —— 静默建目录会让两端从此分叉。需要先让 omp 打开过该项目。
learn把一条持久教训写进本项目的共享记忆。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
memory | string | 是 | 教训本身。一条独立的陈述句。 |
context | string | 否 | 简短来源(文件路径、命令、报错),供后来者判断可信度。 |
写入失败时抛错并说明原因(例如「拒绝写入:本次写入将淘汰最旧的 1 条既有教训」)。
memory_read按行分页读取本项目的完整记忆,只读。用于读到被注入窗口省略掉的教训,或审计记忆实际内容。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
offset | integer | 否 | 行偏移,默认 0 |
limit | integer | 否 | 返回行数,默认 50,上限 maxPageLines |
| omp | 本插件 | |
|---|---|---|
| 记忆文件 | ~/.omp/agent/memories/<项目键>/learned.md | 同一个文件 |
| 写入方式 | Bun.write(非原子) | writeFileAtomic(原子) |
| 淘汰策略 | 静默淘汰最旧的 | 默认拒绝并回报(可切到 omp-parity) |
| 注入预算 | 5000 token | 5000 token(一致) |
| 脱敏规则 | 上游实现 | 逐字移植,差分测试保证字节一致 |
omp 的 config.yml、autolearn、managed-skills 一律不碰。
单元测试能证明「实现符合我们写下的规则」,但不能证明「两端真的看到同一份东西」。 所以另有一组黑盒对跑:同一句提示词,分别喂给 omp 的无头模式与 DSH 的一次性任务, 再用一个没有记忆的目录做对照。
| 有记忆的项目 | 没有记忆的目录 | |
|---|---|---|
omp(omp -p) | 答出记忆里的正确答案 | 明确回答「没有这条记忆」 |
DSH(dsh --profile headless) | 答出同一句正确答案 | 明确回答「没有这条记忆」 |
反方向也验证过:DSH 的 learn 写进去一条带唯一标记的教训后,
omp 在同一个项目里能读到它。
对照组的意义:正确答案只有在两边都拿到记忆时才出现 ⇒ 它来自记忆,不是模型猜的。
managed-skills/,不做 auto-learn 的自进化。root。 omp 在 Linux 上用 $XDG_STATE_HOME/omp/memories,
本插件的默认是 ~/.omp/agent/memories,两者不一致时需要手工指定。Bun.write)在写入瞬间可能让 DSH 读到半截文件。
撕裂守卫会检出并重试,但极端情况下仍可能读到不完整内容。requireApproval 默认关闭。 DSH 没有工具级审批字段,审批只能通过
tools/pre-execute 的 waterfall 模拟,且必须有审批服务在场,否则 ask 降级为拒绝。v18.12.2 的快照。 上游改进后需要重新对齐(重跑 npm run test:fixture 会报漂移)。password=... 仍会被原样注入。收紧读侧会破坏与 omp 的字节一致性,
这是一个已知的、刻意保留的不对称。memory_read 主动读。(读取预算是按字符算的,实际 I/O 按字节算,中文约 ×2.7。)npm install
npm run check # typecheck + vitest + fixture 三连
npm run build # 产出 lib/(逐文件编译,不是一个 bundle)
npm run test # 只跑 vitest
npm run test:fixture # 上游漂移检查 / oracle 自检 / 安全红线脚本
npm run test:upstream # 在线拉取 GitHub 上的 omp 固定 tag 比对 sha256
npm run verify:build # 把构建产物装进真实 cordis 容器并加载
npm run verify:package # 打包 → 解包 → 逐个解析相对 import → 真的 import 一次
verify:package不是形式检查:它会真的npm pack,解包,验证产物里每个相对 import 都能解析(漏发一个.js文件就会在这里炸),然后真的 import 一次产物入口。
test/fixtures/omp-v18.12.2/ 里是 oh-my-pi v18.12.2 的上游源码切片
(memories/index.ts、memories/storage.ts、memory-backend/redact.ts、
prompts/memories/read-path.md)以及一份 manifest.json(逐文件 sha256)。
check.mjs —— 校验本地切片与 manifest 一致,并把 pure.ts 重新抽取后逐字节比对(oracle 手改即漂移)selftest.mjs —— oracle 自身的行为自检,并全量扫描真实记忆目录验不变式safety.mjs —— C6 安全红线的可执行判据verify-upstream.mjs —— 在线拉取 GitHub 上的 omp 固定 tag 比对 sha256pure.ts —— 从上游切片抽出的纯逻辑,测试专用npm run test:fixture 全绿是放行的前提之一。
上游升级后:
node test/fixtures/omp-v18.12.2/regen.mjs
MIT
上游 oh-my-pi 的相关代码遵循其自身许可证;本仓库只包含为差分测试而保留的源码切片。