🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI
广告☁️ 云服务器特惠阿里云首购 8 折 · 腾讯云合作特惠
DeepSeek Harness Hub
← 返回列表

Marquez807/dsh-experience-memory

DeepSeek Harnessspec-screened扫描:中风险在 GitHub 查看 ↗
需源码安装

经验记忆@marquez807/dsh-experience-memory

暂不能直接安装(需源码编译或环境不满足):仓库 package.json 标记 private,未发布到 npm,需从源码安装。 · 最近上游提交 2026/9/25 · 已提供中文文档

DSH 的跨会话经验记忆:只有带有可验证来源的经验才能到达模型,每一轮都会注入相关的经验,而无人使用的经验会被淘汰。

综合分
30.7
GitHub 分
30.7
用户评分
—
★ Stars
1
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add Marquez807/dsh-experience-memory
仓库 package.json 标记 private,未发布到 npm,需从源码安装,改用 GitHub 源安装
信任档位:需留意静态安装检查未通过
是什么
dsh 原生插件 · chat
装得上吗
静态安装检查未通过,可能需要源码安装
安全吗
本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
还在维护吗
活跃:最近一次提交在 0 天前

档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →

数据截至 2026/9/26(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查需源码安装

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-experience-memory @ 0.3.1
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

仓库 package.json 标记 private,未发布到 npm,需从源码安装

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/23 19:04:18

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-commands@deepseek-ai/dsh-llm@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-tools@deepseek-ai/schemastery
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

由 DeepSeek 最新模型翻译生成
经验记忆(@marquez807/dsh-experience-memory)

简体中文 · English

给 DeepSeek Harness 的分领域长期经验记忆:分得清轻重、攒得下经验、忘得掉过期、纠得了错,并在再次执行同类工作时自动召回相关经验。

- 每轮固定只花 204 字节:一行提示,除此之外只在真有相关经验时才注入内容。
- 运行时零第三方依赖,只用 Node 内置能力;存储是单个 SQLite 文件。
- 五个模型工具、七个斜杠命令,零配置可用。

目录

| 想做什么 | 去哪一节 |
|---|---|
| 先装上,并确认它真的在工作 | 快速开始 |
| 搞清它靠什么机制记住东西 | 它做什么 |
| 查有哪些工具、哪些斜杠命令、有哪些配置项 | 工具 · 斜杠命令 · 配置 |
| 看它在模型眼里长什么样 | 模型的体验 |
| 知道它做不到什么 | 已知限制与推迟的事 |
| 从旧记忆库搬数据进来 | 迁移 |
| 想改代码、编译、跑测试 | docs/DEVELOPING.md |

快速开始

装(把路径换成你手上的 tarball):

dsh plugin --profile  add /path/to/dsh-experience-memory.tgz

包名是 @marquez807/dsh-experience-memory(带 scope),这是故意的。 npm 上另有一个同名的
dsh-experience-memory 属于别人:桌面版按包名解析依赖,所以只要按名字装,就会装到那一家
(它缺数据库二进制,一加载就崩,整个后台起不来、所有第三方插件停用——2026-09-23 真实发生过)。
带 scope 之后,按名字装只会得到"没有这个包"的明确报错,不会再静默装成别人的东西。
本仓库同时是 private: true:不发 npm,安装只走本仓库 Release 的 tarball。
判断手上是哪一份,看 package.json 里的 repository 是不是 Marquez807/dsh-experience-memory。

这一步就够了。 dsh plugin add 不只是装依赖——它会把 dsh.profile.bundles 与已安装状态对账:任何声明了 dsh.bundle 的依赖都会被自动追加进 layer stack(见 @deepseek-ai/dsh 的 reconcilePlugins)。所以不需要手工编辑 profile 的 package.json。

装完重启应用即可。零配置:不提供任何 config 也能工作——默认库在 $DSH_HOME/experience-memory/memory.db 自动建立,五个工具、七个斜杠命令与常驻注入立即生效。

重启后先看一眼启动日志那一行(这一行是刻意加的,来由见「已知限制」里那次事故):

experience-memory: store  —  records,  confirmed,  anchored

确认 store  是不是你预期的那个库。"空库"和"开错库"从外面看一模一样(都是"什么都查不到"),所以这一行把路径和条数说明白——库开错了就看得出来,不会静默地什么都不告诉你。三个数字随库变化,多少都不用管;要警觉的是路径不对,或者 0 records。

想在装之前/装完之后确认它是在工作的,用斜杠命令:

/memory-status          # 库里有多少、多少条够常驻线
/memory-preview 部署     # 这一轮实际会注入什么

还有一条只在维护轮次里跑的自检:某条记录引的那个文件如果已经不在了(被删、被改名),维护会把这条记录标上 needs_review 并写明缺的是哪个文件。只标记、不拒绝——文件可能是"以后才创建"的。想现在就跑一遍,用仓库里的 tools/provenance-audit.mjs。

它挂了四个表面

| 表面 | 内容 | 谁触发 |
|---|---|---|
| 自动注入 | 常驻摘要:核心层(跨项目印证过)+ 查询层,共享 1536 字节;外加一行每轮固定出现的经验提示(204 字节) | 无 |
| 自动维护 | agent/turn-stopping 有界维护,批量 32 条带游标 | 无 |
| 模型工具(5 个) | memory_recall / memory_remember / memory_feedback / memory_forget / memory_stats | 模型 |
| 斜杠命令(7 个) | 状态、预览、维护、审计、导入、采集复核、反复失败 | 人 |

工具和命令的分工是刻意的:审计与导入会伸到库外面(扫描任意目录、批量写入),所以留在人的触发之后。memory_stats 是唯一给模型的运维视角工具——只读、无参数,用来回答「你记得什么」,或者自查「我记的东西到底有没有送达」。

那一行经验提示为什么必须独立于摘要、且无条件出现:摘要在没有合格记录时渲染空串(不注入),而"库里什么都没有"正是模型最需要被告知"可以记录"的时刻。把它并进摘要,它就会随着记忆一起消失——而库空着这件事会自我维持。这不是推测,是实测:在 5 个真实会话、约 5,900 次工具调用里,记忆工具在装好之后的每一个请求轮次都被提供了,而 memory_remember 一次都没被调用过,直到有人明确点名要求记录。

同一句话现在也用来要求"查"。 见下面「记了不等于有用」:只叫模型记、不叫它查,等于让它一直写、从不读。所以提示的顺序是先查后记——动手前 memory_recall,学到东西 memory_remember,用过 memory_feedback。

它做什么

三个阶段各有一层机制,外加两条把机制串起来的经验:记了不等于有用,以及记了,但没在它动手的那一刻出现。

1. 记录时——证据定级

记录一条经验必须给出它所依据的原文(quote)和出处(source_ref)。插件自己去核对:

| 等级 | 条件 | 基础分(×3.0) |
|---|---|---|
| verified-tool | source_ref 是本会话里一次真实执行且未报错的工具调用 | 9.0 |
| verified-user | 原文逐字出现在用户发出的消息里,且该句不是疑问或假设 | 7.5 |
| verified-file | 原文出现在所引用的工作区文件里 | 6.0 |
| inferred | 以上都不满足 | 1.5(永远候选) |

只有前三级能进入注入层,inferred 永远是候选。这是必要条件,不是充分条件:常驻资格线是 5.5,
而 verified-file 的基础分是 6.0 —— 高出 0.5,按每天 0.0083 的扣分算是约 60 天。所以三级证据的实际行为是:

- verified-tool / verified-user 从第一天起就能常驻,而且能靠基础分撑很久(9.0 / 7.5 对 5.5,约 360 / 180 天);
- verified-file 靠自己也能常驻约 60 天;60 天里没人查过、也没被确认有用,才会沉到线下,此后需要查询命中一个标识符(路径、类名、文件名,值 1.0 分)或被查过/被成功复用(被查过封顶 +1.0,成功复用每次 +1.5 的对数分)才回到线上。线下时它仍然在 memory_recall 里按需可检索。

那 0.5 是刻意留的。 它原先不存在:资格线曾经也是 6.0,与 verified-file 的基础分精确相等,于是任何年龄扣分都把它压到线下 —— 那不是"靠相关性换位置",是"必须在写下的那一瞬间被使用",实际等于永远不用。现在这条间隔是一条有意画的线:新记忆白送两个月曝光,之后要靠被用来续命。

这是刻意的:文件里读到的事实比工具实测和用户断言弱,让它靠「与本轮相关」而不是靠「存在」换取提示词位置。
审计里那些 verified-file 记录实测有一部分立即合格(够新的都合格)、命中标识符后合格率更高——这正是该规则在工作。

1.1 失败必须被说出来

判定逻辑不改,但失败的原因要外传。这条是被一份调用方缺陷工单逼出来的:对方为了搞清自己三条记录为什么只拿到 inferred,
做了 5 次记录实验、通读源码,才发现真因是"我给的是绝对路径,插件根本没读"和"引文漏了一处 "——
而这两件事,写入返回体里一行字就能说清。

改之前,四个不同的失败(绝对路径 / 越界 / 文件不存在 / 读不了)全部汇成同一句
no session or workspace evidence matched the supplied passage——这句话指着引文,而真因在路径上,是典型的把人引向错误方向。

现在 readWorkspaceFile 把失败原因作为数据返回,reason 逐条说清试过什么:

| 失败 | reason 现在怎么写 |
|---|---|
| 绝对路径 | 点名它是绝对路径 + 要求改成工作区相对路径 + 给出工作区根 |
| 文件不存在 | 给出被引路径 + 列出最近存在目录的内容(仓库在 repos/x/ 下而调用方写了 lib/y.js 时,一眼可见) |
| 路径越界 / 读不了 | 各自独立成句 |
| 引文不在文件里 | 若忽略 markdown 装饰符后能匹配,就明说这一点并让它整行复制;否则给出最接近的第几行及其内容 |

两条刻意的边界:装饰符只用于诊断,不用于放行——忽略装饰符后匹配仍然判 inferred,逐字契约没有被软化;
以及每次判定都带 route(tool-call / file / user-message / none),因为 source_ref 是双关字段
(工具调用 id 或 path:line),调用方此前无法知道自己写的到底被当成了哪一种。

1.2 grade 是写入时冻结的

证据等级在写入那一刻定下来、此后不再重算;每次召回重算的是 importance(它由已存的事实推出:年龄、复用、失败连击)。
所以所引文件后来被移动或删掉,不会改变这条记录的证据等级——它仍带着当时的结论,也不再可被任何人复核。
工作区归属同理:workspace_id 在写入时由会话 cwd 解析,换一个工作区后这条记录是看不见(而不是"等级变了"),
除非它已经升到领域级。

1.3 进入注入层还有第二道闸:相关性

定级管的是「这条值不值得信」,相关性管的是「这一轮是不是在讲这件事」,两道闸相互独立,都要过:

| 闸 | 判据 | 过的条件 |
|---|---|---|
| 定级 | importance ≥ 6.0(证据 + 历史) | 见上 |
| 相关性 | 与当轮查询的词元重合是否具体 | 命中标识符,或至少共享一个实词 |

第二道闸是实测补上的。此前只查定级,于是出现过这样一次注入:一条讲 batchSize 上限 500 的记录,被注进了
「把这个仓库的 README 用一句话改写」这一轮——两者语义毫无关系。唯一的原因是 FTS5 的表达式是按二元组 OR 匹配,
而那条记录的正文里有一句"不得动这个值",撞上了提问里的「这个」。确定性复现:identifierMatches=0、bm25 仅 −0.59、
excluded 为空——没有任何过滤器提出异议。

问题的形状是「常用词不构成相关性证据」。判据因此不是"共享几个词"(两字中文词只产生一个二元组,要求多个会把
显然正确的匹配一起拒掉——第一版就是这么做,被测试当场否掉),而是「共享的那个词是不是实词」:src/retrieve.ts
里维护一张 CJK 功能词表(这个/可以/一句/…),只有共享词全是功能词时才拒绝。修的是根因,不误伤"只共享一个实词"的正当匹配。

一个反向激励也一并消失:importance 随成功复用上升,所以越有用的记录越容易越过定级线,只查定级的话它同时就越容易
靠一个"这个"漏进无关回合。现在相关性那道闸与历史无关。

2. 召回时——分清轻重

旧系统要求人工登记脚本哈希并重放 2–32 次才允许晋升,机制严谨但代价致命——139 个工作周期后记忆库里 0 条稳定资料。这里的定级是自动的,因为只有便宜到会真的发生,严格才有意义。

常驻层每轮由 ctx.systemPrompt.context 重新求值(不是开机快照),最多两段、硬上限 1536 字节:

经验记忆(领域通用,已由多个项目独立印证):
- [id] 标题 — 教训          ← 核心层:不管这一轮在说什么都在
Experience memory (relevant to this round):
- [id] Title — Lesson          ← Query layer: hits the current topic

The two sections share the same 1536-byte budget. This is why "unconditional injection" is affordable: what the core layer consumes is a
reallocation of the prompt, not an addition—it cannot conjure more tokens. If a section has no content, the entire section does not appear (no empty heading is left behind);
when there is only the query layer, usage is exactly the same as with a single section.

The reason the core layer exists: the query layer is query-gated, so when the user replies "continue," there are no tokens to hit, and the summary happens to be
cleared during a long task. The admission conditions for the core layer are the narrowest in the entire framework:

| Condition | Why |
|---|---|
| scope = domain | Only content independently reported by two or more workspaces is promoted to domain level |
| status = confirmed | Candidates are never injected |
| evidence ≠ inferred | Content that nothing has verified is not injected |
| distinctWorkspaces ≥ 2 | One project's habit is not a domain rule |
| Passes the same residency eligibility line as the query layer | Core records are always a subset of the resident layer, not a back door |
| Limited in count by coreMaxRecords | Ensures it is bounded |

Workspace-level records will never become core no matter how important they are—nothing has corroborated them.

Within the hit set, sort by this formula:

importance = 3.0 × evidence level          (verified-tool 3.0 / user 2.5 / file 2.0 / inferred 0.5)
+ 1.5 × log2(1 + successful reuse count)
− 2.0 × consecutive failure count
− 1.5 × staleness
+ 0.5 × log2(number of independent workspaces)
+ 0.3 × log2(1 + reuse count)
+ min(2.0, 1.0 × exact identifier hit count)      ← capped

Sort key importance DESC, bm25 ASC, id ASC. The old system sorted the 8 resident items by uuid4 string, which is equivalent to random sampling and permanently frozen—when there are 100 items in the library, the probability that a new memory enters the overview is only 8%.

2.1 Recording does not equal usefulness: a death loop that turns memory into "write-only, never read"

This is the issue the user directly pointed out: "what is recorded is not used." The reason found is not that the agent is not self-motivated, but that four things form a closed loop:

1. To automatically appear in the prompt, importance must be ≥ 6.0;
2. A file-level memory is exactly 6.0 when just written (3.0 × 2.0)—a knife-edge on the threshold, and a few hours of staleness penalty pushes it below the line;
3. To stay above the line, it can only rely on reuse bonus, which requires someone to call memory_feedback to say "this one helped me"—this action has occurred only 3 times in the lifecycle of all 76 items in the library;
4. So of the 76 items, only 2 remain in the automatic layer, and the rest can only be found by the model actively calling memory_recall; and that unconditional prompt only tells it to record, never tells it to query. Worse: the act of "querying" is not recorded at all, so even if some item is later dug up and used by a subsequent session, the benefit it receives is zero—next time it will still remain silent.

All four places were fixed:

| Change | Effect |
|---|---|
| memory_recall now records "this item was queried" (retrieve_count / last_retrieved_at) | Retrieval leaves a trace for the first time, and the question "has what was recorded been used" can finally be answered |
| Being queried also counts as "touched": the staleness anchor takes max(creation time, last used, last queried) | A memory later dug up and used no longer decays as if "no one cared about it", and it will climb back into the automatic layer by itself—the death loop is broken |
| Retrieval bonus is capped at 1.0 (0.3·log2(1+query count), not exceeding 1.0) | One query is worth less than one "recorded success"; otherwise repeatedly calling memory_recall could make anything permanently resident |
| The prompt is changed to query first, then record, and names memory_feedback | The prompt is an existing means of fixing "provided but unused" (see the real test above), and this time it is applied symmetrically to "querying" |

Automatic injection does not count as "queried", and this is deliberate: if a record counts its own injection as usage, it will keep itself in the automatic layer, and that number will no longer mean "someone looked for it."

memory_stats therefore has one more line, directly answering this question: Queried N/M items (within the confirmed scope) · K items never queried and never confirmed useful. K is the stock of "write-only, never read"; it should decline as sessions progress.

2.2 Recorded, but it did not appear at the moment it took action

This is the second issue the user pointed out, and it is more subtle than "recorded but unused": that memory really exists, is really correct, and really was injected,
but it happened not to appear in the round when it should have.

A real example: a record saying "before launching Bannerlord, you must confirm Steam is logged in, otherwise the game silently exits after 10 seconds,"
with file-level evidence, was injected in 9 of the 15 rounds of that session—but not in the round when the user said "let's start."
The agent launched directly, and that round was wasted.

There are two reasons, and neither is "the memory is broken"; it is "the delivery method is wrong":

1. The sentence used to find memories contains only what the user said. When the user replies "let's start," there is nothing in those words that can hit
"Steam" or "launch." So the summary layer happens to be cleared during a long task, and what the agent is currently doing does not enter the query at all.
2. There is only one delivery timing, "the beginning of each round," and this timing is determined by the user's words, not by what the agent is doing.

Two places were changed, both measured against the real logs of that session (444 tool calls), not inferred:

- Add "what the agent is doing" to the query: the parameters of the tools it calls, the words it writes itself, and its to-do list.
Messages injected by the plugin itself are always skipped, otherwise a prompt would feed itself back into the next round's query. When there is no activity,
the assembled query is exactly the same as before—this is pinned down by an assertion.
- Deliver when a tool call is about to act (precall): the record itself declares which calls it applies to—when writing a memory,
fill in recall_for, whose value is one of three facts: path: (this call will name this file),
tool: (this call is this tool), command: (this word appears in the command line).
If a call satisfies one of these, that record is delivered; at most one per call, at most 300 bytes; records that do not declare this will not appear before action,
and only enter the per-round summary and are found only by memory_recall.

Why no longer "whatever is in the call collides with whatever is in the record": that criterion projected onto 15,383 real calls hit 57%
of calls; sampling 47 items for manual item-by-item review found only 5 truly relevant (10.6%), and 68.7% of deliveries hit a word
that appeared only in the record's body body, while the rule the record itself states (trigger/failure_mode/lesson) did not
It didn't mention it. Tightening the threshold enough to remove noise dropped recall on the 25 manually annotated scenarios to single digits—and even under the loosest configuration,
only 14 correct records made it into the candidates; the correct ones were never selected at all, and re-ranking couldn't save it. So this isn't something parameter tuning can fix:
"two words collided" does not imply "this experience applies to this invocation." For the full empirical results and failure paths, see
docs/DELIVERY-GAPS.md sections 12 and 13.

Three things the old mechanism tried, measured, and deleted (all because replay said they didn't work, not out of laziness. They're kept here to explain
"why it wasn't done that way"—these three belong to the word-matching approach that has already been replaced):

| Approach tried | Replay result |
|---|---|
| Treating parameter key names as cues too (file_path, old_string) | Every edit carries these keys, so 232 of 444 invocations hit something; what got selected wasn't the record that should have been looked at. Changed to reading values only |
| Only one per turn (tried 1 through 6) | That turn's slot got taken by "some other record encountered earlier in the turn," and the Steam one was never delivered even once. So throttling relies only on "cooldown for the same record" and "a per-session cap," and the code documents why |
| Using Bannerlord as a cue | 13 of the 17 records visible in this workspace mention it, so hitting it is as good as not hitting it; whereas launch-a-runtime-clean.ps1 appears in only 2 and ERC403 in only 1—those are what this experience is actually about |

The old mechanism's effect on that real session was 20 hints, landing in 4 of 15 turns (the Steam one was attached to the "write launch script"
invocation—the same turn as launching the game, before taking action). These are the numbers for the mechanism that was replaced, kept as a
control: the new mechanism's corresponding figures are "delivery rate 1.18%, max false triggers for a single record 83 times" (see "Whether it's actually useful" below);
the two aren't the same quantity at all—the old one fired a lot but irrelevantly, the new one fires little but only where records explicitly named it.

Let's be clear about what it can't do: it does not guarantee attaching to the invocation where it would be most useful. The first action in a turn that touches the matter
gets the slot first, so the "run" invocation might not get it—the experience is already in the same turn's conversation, but it isn't
"attached to that line." This is a real trade-off, written here rather than glossed over.

3. Afterwards—forgetting and correction

- Retirement: user explicitly forgets / 2 consecutive failure results / expired / review overdue and never reused / unused for 90 days and score below threshold
- No physical deletion: retirement is reversible; only purge=true deletes bytes
- Cross-project promotion: a record stays only in the workspace where it was learned, until two different workspaces independently report the same content; then it's promoted to domain level, finalized, and becomes core memory unconditionally injected every turn
- Identity is the assertion itself, not the title: the title is just a label (often an automatic summary of the body), so two records with the same body and different titles are the same knowledge. Counting the title in identity would make cross-project corroboration never count, and domain promotion would never happen
- Re-recording retires the candidate it supersedes: the model has a stable habit—first write a version without citations (→ candidate), realize it's inadequate, then rewrite with file citations. Because identity is the assertion, the rewritten body is another record, and the candidate stays in the store forever: not injectable, not visible, and nothing cleans it up. In practice, 3 such duplicate pairs formed in one real store (43% of all records). Now when a graded record is written, candidates in the same workspace with the same title are retired, supersededBy points to the new record, and a correction log is written. Title comparison folds punctuation—the store has a pair differing only by a pair of 「」, and exact comparison treated them as two different claims
- Maintenance runs at agent/turn-stopping, in batches of 32 with a cursor, and never enters the retrieval hot path

3.1 Perishable facts: putting an expiry window on records

Long-term memory that never expires is a liability—assertions like "the current test command is X" or "the current client version is 1.5.2" will silently become false after the world changes,
and because they're verified facts, they rank even higher. So memory_remember accepts two optional windows:

| Parameter | Effect |
|---|---|
| expires_in_days | Immediately stops being retrieved upon expiry; maintenance then changes status to retired |
| review_after_days | Upon expiry, does not retire directly but requires review; if after another 30 days (REVIEW_GRACE_DAYS) it's still never reused, then it retires |

The division of labor between the two rules is deliberate: expired facts shouldn't be answered, but "needs review" doesn't mean "already wrong." Moreover, records that have been reused won't
retire due to overdue review—the review window is for finding things nobody needs, not for punishing age.

Reporting the same assertion again is re-verification: the new window replaces the old one rather than being ignored.

Before these two parameters were added, expiresAt and reviewAfter were only filled in by the old data importer, so two of the three retirement paths
were forever unreachable for records the plugin itself recorded—the mechanism was complete but nobody could trigger it.

Operations

Scope

| Scope | Who can see it |
|---|---|
| workspace | Only workspaces that resolve to the same root path |
| domain | Any workspace that resolves to the same domain |

Domain resolution order (first hit wins): plugin config defaultDomain → workspace .dsh/memory.yml's domain: → package.json's name → git remote repo name → left empty (workspace-level only).

The last level is deliberately left empty rather than using the directory name: treating a name like dsh主工作区 as a domain would spread a single project's quirks to all directories with the same name.

Turning off memory for a certain mode (e.g., "model test mode")

A mode (agent preset) can't turn off this plugin itself: the plugin is installed at the profile layer, and disabled in a mode only applies to the lines it declares itself.
So the switch is on the plugin side, keyed by mode id (disabledPresets, empty by default). For modes listed there, the session gets:

| What's turned off | Why |
|---|---|
| Experience summary injection | This is the most direct manifestation of "memory"; once the model sees it, it's no longer "bare" |
| "Check before recording" hint | It's telling the model "memory is available," and test mode shouldn't be prompted that way |
| Pre-action reminder | Same as above, and it would stuff historical experience into a particular tool invocation |
| Candidate collection + failure statistics | Not recorded: test session turns shouldn't enter the store |
| Behavior of the five memory_ tools | When called, they directly refuse and explain why (the tool table itself is hidden by the mode, see below) |

The determination reads the agentPreset in the session header, and also looks at the agent-preset/selected event—so a session that "starts in standard mode, then switches to test mode"
also counts as test mode (reading only the header would miss it, reading only events would miss all sessions that never switched; reading both is correct).
工具表怎么消失:模式里挂一个本地小插件调 tools.restrict({deny})——工具注册表只允许作用域内的限制
(全局限制会把所有 agent 的工具都蒙掉,所以 API 直接拒绝),而模式正好是一个作用域。两道防线是独立的:
配置那层管“不注入不记录”,模式那层管“工具表里没有”。

维护仍然会跑:它是库的卫生工作(过期、淘汰),任何会话都看不见它。跳过它只会让“无记忆模式”悄悄让全库停止老化。

工具

| 工具 | 作用 |
|---|---|
| memory_recall | 按查询检索,上限 16384 字节,超限按序截断并报告。include_candidates 用来复核自己记过但没验证过的断言,include_retired 用来审计已退役的。只在真正交出去的那些记录上记一笔“被查过”(截断掉的尾巴不算),这是“记忆有没有被用”的唯一痕迹 |
| memory_remember | 记录一条事实/经验/策略;不提供可验证原文则存为候选。可选 expires_in_days / review_after_days 给易腐事实上一道窗口;可选 recall_for 声明这条记录适用哪些调用,填了才会在动手前递出去(见下) |
| memory_feedback | 关联一次真实结果;成功清除失败连击,两次连续失败即退役 |
| memory_forget | 退役(默认)或彻底删除 |
| memory_stats | 只读普查:库里有几条、多少条够常驻线、复用与纠错计数、最近退役原因。无参数。首行是构建标识、末行是本调用的 call id,/memory-status 是它的给人版本 |

两个工具的描述是指令性的,不是能力说明:memory_remember 以触发时机开头(“一旦学到下次会话仍然成立的东西就调用”),memory_recall 以适用场合开头(“进入不熟悉的领域、或可能要重复一个已经做过的决定之前调用”)。理由是实测出来的——仅仅把工具放进 schema 不足以让模型使用它(见上文的 5,900 次工具调用)。约束写在描述末尾:只记可复用的规则,不记一次性细节、瞬时工具输出、密钥或未经验证的猜测。

source_ref 的参数说明还写明了哪条引文是可以定级的:依据文件就写 path/file:line;主张“某个命令能用”就引用成功的工具调用 id;而从失败中学到的教训不能引用那次失败调用——失败调用在此不构成证据(gradeEvidence 的既有语义,evidence.test 里钉着 "a cited tool call that errored proves nothing")——应改为引用记录了该发现的那个文件。这一句是实测补上的:一个隔离回合里模型把失败的 pytest 调用当出处,记录于是只能落成候选、永远够不到常驻线;而它在另一次里自己绕到了“引用写进仓库的测试文件”这条可定级路径,只是多花了一轮。

quote 还有一条硬要求:引文本身要说出那条规则(一条规则、一个顺序、一个值、一条报错),不是“作者当时正在读的那一段”。依据就是模型自己的判断——一条讲部署 vault 的主张配上一句“怎么在本地起服务”的引文,它的回答是 "the cited evidence does not match the claim",然后把整条记录丢掉。verified-file 只能证明“这句话在那个文件里”,证明不了“这句话讲的就是这个主张”;后者是语义判断,本框架刻意不做模型调用。所以退路也写在参数说明里:找不到这样的句子就写 inferred 那一档、并说明缺什么。

recall_for 决定它会不会在动手前递到眼前,取值是三选一的事实:

| 取值 | 含义 | 什么时候用 |
|---|---|---|
| path: | 这次调用要点名这个文件 | 教训是关于某个文件/某类文件的 |
| tool: | 这次调用就是这个工具 | 教训是关于怎么用某个工具的 |
| command: | 命令行里出现这个词 | 教训是关于某条命令的 |

没填就不会在动手前出现——只进每轮摘要、只被 memory_recall 搜到。这是刻意的取舍,代价与理由见上面「召回时——分清轻重」那一节:不是“填不填”的选择题,是没声明就等于没有动手前这一层。

斜杠命令(给人用,模型看不到)

通过 ctx.commands.register 注册,所以出现在 /compact、/goal 所在的同一个斜杠菜单里。全部 recordInput: false——运维命令和文件系统路径不会进入会话记录。

| 命令 | 用法 | 作用 |
|---|---|---|
| /memory-status | — | 库普查:条数、状态/证据/作用域分布、多少条够常驻线、复用与纠错计数、最近退役记录及原因 |
| /memory-preview | [] | 打印该查询下实际会被注入的摘要,以及按需检索会补上什么。不传 query 时用最近两条用户消息——与插件自己的查询推导是同一套逻辑 |
| /memory-maintain | — | 立刻跑一次有界维护并报告退役了几条、为什么(同一套规则每轮结束也会自动跑) |
| /memory-harvest | [--retire ] | 列出自动采集的候选,或退役其中一条 |
| /memory-audit |  [--out ] | 审计归档库的正确性并落盘四份报告 |
| /memory-import |  [--selection ] [--apply] | 默认只试运行;只有显式加 --apply 才写入 |
| /memory-gaps | [] | 列出本工作区反复失败的形状、实际报错、库里有没有相关的记录,以及哪条记录写了却没挡住。只统计,不注入、不写记录 |

为什么审计与导入不给模型:它们会扫描任意目录并批量写库,爆炸半径大,而这个框架一贯 fail-closed。模型的工具表因此只有 5 个(其中 4 个是知识操作,第 5 个是无参数的只读普查),不牺牲每轮 token。

/memory-preview 与真实注入共用同一个函数(src/digest.ts),所以它不可能与你实际收到的内容不一致——一个会漂移的预览就没有存在意义。

配置

| 键 | 默认 | 含义 |
|---|---|---|
| enabled | true | 整体开关 |
| dbPath | $DSH_HOME/experience-memory/memory.db | 数据库位置 |
| residentMaxRecords | 5 | 每段条数上限 |
| residentMaxBytes | 1536 | 整个摘要(所有段合计)的字节硬上限 |
| coreMaxRecords | 2 | 核心层条数上限;0 关闭核心层 |
| standingMaxRecords | 3 | 常驻规矩条数上限;0 关闭常驻层 |
| standingMaxBytes | 768 | 常驻规矩那一段自己的字节上限(整份摘要仍受 residentMaxBytes 约束)。按实测每行最多 240 字节、标签 49 字节,这一段装得下约 3 条短规矩或 2 条长规矩 |
| recallMaxBytes | 16384 | 单次召回字节上限 |
| defaultDomain | '' | 固定领域;空则推断 |
| maintenanceBatchSize | 32 | 每次维护处理的记录数 |
| failStreakLimit | 2 | 连续失败几次退役 |
| harvestEnabled | true | 是否在每轮结束时自动采集候选 |
| harvestBroad | false | 是否启用实测不可靠的宽判据(宽陈述句、失败后成功、目标变更) |
| harvestMaxPerTurn | 1 | 每轮最多采集几条(0 = 关闭采集) |
| harvestPoolLimit | 200 | 候选池上限,超了退役最旧的 |
| harvestCandidateTtlDays | 14 | 候选多少天没人确认也没被查过就退役 |
| precallEnabled | true | 是否在工具调用即将做某件事时,把关于那件事的经验递到它眼前 |
| precallMaxPerSession | 20 | 一个会话最多提醒几条(按真正递出去的条数算) |
| precallCooldownMinutes | 30 | 同一条记录多少分钟内不重复提醒 |
| failureTracking | true | 是否统计本工作区反复出现的工具失败(只统计:不注入、不写记录) |
| failureShapeLimit | 200 | 每工作区最多留多少种失败形状,超了淘汰最少最旧的 |
| disabledPresets | [] | 哪些模式完全没有记忆(按模式的 id 填)。列进去的模式:不注入、不提醒、不采集、不统计,工具被调用时直接拒绝 |
| anchorCostTable | true | 声明锚点时先查它"有多贵":一个锚点如果在真实调用里命中太多次,丢掉它并告诉你(记录照写,摘要与检索不变)。防的是"一条记录吃掉整个工作区的提示预算" |
| anchorCostMaxHits | 300 | 命中多少次算"太常见"。默认与预注册的单记录门槛同值( # 只导清单里的
node tools/import-legacy.mjs --root "F:\GPT工作区" --apply     # 写入(不带清单就是全部可映射记录)

插件内(装完即可用,无需仓库):

/memory-audit "F:\GPT工作区"
/memory-import "F:\GPT工作区" --selection "…\legacy-memory-selection.json"
/memory-import "F:\GPT工作区" --selection "…\legacy-memory-selection.json" --apply

默认只试运行,因为归档树里既有活库也有副本,误导入不是可逆的错误。

判断与机械操作分开

「哪些记录值得导入」是关于数据的编辑判断,「把记录写进库」是机械操作。两者被拆开了:

- tools/audit-legacy.mjs 做判断,并写出 legacy-memory-selection.json —— 纯 JSON,就是给你改的。
删掉你不同意的条目,然后:

node tools/import-legacy.mjs --root "F:\GPT工作区" --selection audit\legacy-memory-selection.json
node tools/import-legacy.mjs --root "F:\GPT工作区" --selection audit\legacy-memory-selection.json --apply

- tools/import-legacy.mjs 只执行清单。试运行会报告清单排除了多少条,所以在写任何东西之前就能复核。
- 清单里的身份是 (workspaceId, contentFingerprint),与审计去重时用的键一致,所以它不可能含糊地指向两条记录;它也不依赖记录 id,因为 id 每次导入都会重新生成。
- 空清单是合法答案:导入 0 条,而不是「没给清单就导全部」。

五条刻意的取舍:

- 导入记录直接写入,不重新定级。走 remember 会把每一条都定成 inferred(迁移没有会话可引用),等于在入库路上把一库已验证事实静默降级。
- 旧 global 记录降为工作区级。无法判断它原本属于哪个领域,而广播到所有项目正是新作用域规则要防的泄漏。数量会单独报出来,供逐条决定。
- 副本库不导入。.codex/project-memory-backups/、.dev-packages/、.eval-pilots/ 以及名字里带 backup/snapshot/copy/rehearsal 的目录装的是另一个库的副本。导入它们会让一条经验按快照数量翻倍——归档树里一条记录被存了 34 份。扫描阶段就排除,并逐个列出原因。
- 同一个库内的重复写入合并。旧运行时把同一断言反复追加(迁移过的库还在 entries.jsonl 和 memory.sqlite3 里各存一份),时间戳不同不算新知识。
- 工具失败事件不导入,哪怕它的 type 是 fact。旧运行时在工具调用失败时写的是 type: fact 加 admission.proof.kind: tool,于是它带着最强证据等级和 confirmed 进来,而整条记录只有一句
Tool call_00_... exited 1——没有命令、没有错误、没有修复办法。活库里这样的记录有 98 条,
按证据分排序会排在所有真经验之上。按 type 过滤事件挡不住它们,必须按正文形状挡。

工作历史事件不导入:旧运行时把 failure/task/decision/fix 事件和知识记录写在同一流里。事件是观察,不是教训——一条 failure 说明东西坏了,没说下次该怎么做。把它们当经验导入,正是常驻阈值要挡住的那种噪声。

导入前先审计

迁移工具回答「什么能导」,审计工具回答「这些经验是不是对的」——后者必须在前:

node tools/audit-legacy.mjs --root "F:\GPT工作区"
产出四份:
audit/legacy-memory-audit.md         结论:机械验证 + 漏斗 + 注入行为实测
audit/legacy-memory-recommended.md   建议子集:按项目/主题归类,逐条列出
audit/legacy-memory-selection.json   建议子集的可执行清单,供 --selection 使用,可直接编辑
audit/legacy-memory-records.tsv      全部可映射记录的正文全文

能机械验证的部分它真去验证,而不是猜:

| 检查 | 做法 |
|---|---|
| 引用的路径是否还在 | 对每个绝对路径求最长存在前缀:前缀停在分隔符上说明最后一段真的不在;停在段中间说明路径存在、后面粘的是散文 |
| 引用的命令是否还装着 | 只查真实命令行工具名,不把行内代码里的标识符当命令 |
| 记录之间是否矛盾 | 精确重复(按正文身份)、近重复(词元 Jaccard)、同一主题相反极性(要/不要) |
| 质量信号 | 疑问句、占位符、自指(讲记忆机制自身)、过短、无教训 |
| 导进去会不会真的被注入 | 直接调用框架自己的 importance / eligibleForResident,而不是推断 |

本机归档实测:归档树总计 415 条原始记录,其中只有 6 个活库、324 条;其余 19 个库是副本。
这 324 条里 67 条是同库内重复、98 条是伪装成 fact 的工具失败事件,剩下 150 条可映射。
建议导入子集经漏斗收敛到 36 条:只留 confirmed(−73)、只留有过证据的(−39)、
去掉自指的(−0)、同工作区去重(−0)、正文至少 40 字(−2)。

关于这 36 条是什么,需要一个反直觉的结论:它们全部是 fact,没有一条是 experience 或 strategy。
旧库里没有「教训」这一类知识,只有被切块存进记忆的项目规格、边界和状态台账——版本基线、
范围排除项、安全不变量、里程碑退出门、数据来源授权、当时尚未验证的项。它们在各自项目里很有用,
在别的项目里是噪声,所以都是工作区级而非领域级。

⚠️ 两个必须知道的后果:

1. 旧运行时没有 lesson 和 failure_mode 字段,所以每条导入记录这两个字段都是空的。常驻行是
「标题 — 教训」,教训为空时回退渲染正文,所以导入的记录以正文形式出现,可执行教训这一层是缺的。
2. 它们是 verified-file,而资格线是 5.5、基础分是 6.0,所以够新的导入记录靠年龄自己就能上线;
旧到 60 天以上的,要么查询命中一个标识符、要么被查过/被确认有用才回到线上。所以导入的实际效果是
一个可按需检索的项目知识库,其中较新的那部分还会每轮自动浮现。详见「证据定级」一节。

排查「记忆为什么不出现」

两种原因——库里没有和在库里但进不了提示词——从工具调用里看不出来。

插件内(推荐,装完即可用):

/memory-status              # 库里有多少、多少条够常驻线、为什么有记录退役了
/memory-preview 继续        # 这一轮实际会注入什么

离线(仓库内,可以对任意库文件跑,不必启动 DSH):

node tools/preview.mjs --db  --cwd  --query "继续" --query "WandererProfile"

两者共用 src/census.ts 与 src/digest.ts,所以结论一致。统计里还包含审计轨迹:usage 与 correction 两张表记录每次复用结果和每次纠错,
并列出最近退役的记录及其原因(显式遗忘、连续失败、过期、复核逾期……)。这两张表此前只写不读,
所以「这条为什么掉出池子」在框架里没有答案,只能手工开 SQLite 查。

它加载 lib/ 里的构建产物,所以顺带验证了发布产物与源码行为一致。

已知限制与推迟的工作

这一节用英文标题是为了让锚点稳定(测试按标题逐字定位其中的数字)。

- 「反复犯的错」只被统计,不会被自动写成经验。 这是量过之后的选择,不是省略:七天里本机 63 个会话
产生 358 次工具失败,最常见的一类(改文件前没读,143 次 / 5 个会话)错误信息里就写着怎么做
("read the file, then retry"),前两类合计占 178 次——记忆在那类失败上加不进任何信息,重复是手滑
而不是不知道,而且 harness 的编辑工具本身就是那个守卫。failure-recovered 这条判据本仓库标定过
一次并判为噪音(71 命中 → 5 条算数),这次的数据是支持那次判断,不是推翻它。所以这一版只做
两件不冒险的事:把失败按形状记下来(不注入、不写记录),以及把我们自己的报错写成能照做的
(domain 那条错误进过 Top-10,10 次 / 3 个会话)。判据与数字见 CHANGELOG。
- /memory-gaps 的「相关」是关键词重合度,不是语义覆盖。 错误原文是英文、记录多半是中文,中文记录
可能一条都对不上,所以那个分数只会偏低,报告里也这么写。它的用途是让人看见「这件事一直在发生」,
不是给出「该记一条」的结论。
- /memory-gaps 会指出「哪条记录写了却没挡住」。 判定要同时满足三条:关键词全中(且至少两个词,
一个词的重合是巧合)、记录比这些重复早(一小时宽限,不然新写的记录会被下一次手滑冤枉)、写完
之后又犯了至少 3 次。测得的例子:那条「本机抓不了网页」的经验写于 21:05,之前 web_fetch 三种
失败每小时 0.54/0.34/0.14 次,之后 0.00/0.12/0.00——这是「经验挡住了错误」目前唯一的硬证据。
要复算随时可以跑 audit/verify-prevention-before-after.mjs。
- /memory-gaps 里有些行不是错误。 用户打断计划评审、工具被中止、用户取消等待,都会被记成「失败」
形状——它们是用户的动作,不是 agent 的判断失误。这一版刻意不过滤:过滤要靠一张「这不算错」的字面
清单,而本仓库在这类清单上翻过车(一个词之差就绕过去)。代价是报告前几行可能混着这类行;缓解方式是
每一行都带原始报错,读者一眼能认出来。实测数据支持这个取舍:重启后 19 次失败里有 3 次是这一类。
- 计数只在「回合结束」时读最近一个回合,实测边界(重启后 19 次 vs 逐回合重数 19 次,完全一致):
重启前就已经在跑的回合不会被记(那一版还没这个功能),从头到尾没停过的会话也不会被记。
一致性检查脚本是 audit/diagnose-counter-gap.mjs,随时可以照原样重跑复核。
- 动手前提醒:已经做了,但用的是“记录自己声明适用哪次调用”,不是“照着 trigger 字段猜”。
早先推迟这条是因为那条路实测不可靠:拿“即将调用的工具名出现在某条记录的 trigger 里”当触发条件,
13,198 次调用里会触发 949 次(7.2%),最大触发源是 grep(623 次触发只对应 3 次失败——“grep 断言”
这种句子被误当成触发器),而真正该触发的 web_fetch 反而被淹没。原因不是调参:分辨“这条讲的就是
用这个工具”和“顺带提到这个工具”需要语义判断,本插件不做模型调用;“两个词撞上”推不出“这条经验适用
于这次调用”。所以改成写记忆时用 recall_for 声明(path: / tool: / command:),没声明就
不在动手前出现。预注册的四条判据是这么结的:

| 判据 | 结果 |
|---|---|
| 触发率 ≤2% 的调用 | 1.18%(同一份 15,383 次调用回放) |
| 单条记录误触发 <300 | 83 次(工作区里有三个 tools.js;按文件名锚会撞 508 次,改成相对路径后 83) |
| 不增加每轮固定开销 | 没变,204 字节照旧 |
| 覆盖 ≥15% 的失败 | 撤掉,理由见下面那条 |

代价与边界:动手前这一层只对“用户说过、文件里查不到”的知识实证有效(见下文「它到底有没有用」);
库里声明了锚点的永远是少数,而且这个数随库变化(2026-09-25 在本工作区实测:可投递 234 条,自己声明锚点的 70 条,动手前静默的 107 条;在库所属工作区的根目录跑 node dsh-experience-memory/tools/anchors.mjs 可重测),其余动手前静默——多数讲的是“讨论某项目时”这类没有
文件可锚的事,得人工补 tool: / command: 锚点,这一步没有自动化——tools/backfill-anchors.mjs 只从记录的出处推断 path: 锚点,tool: / command: 仍然要人写。
- “覆盖 ≥15% 的失败”这条判据已撤,换成它本来想表达的那句话:“这条经验写下之后,同类事件还犯不犯?”
撤的理由是实测出来的,不是嫌麻烦:442 次工具失败里 83% 是工具自己拒绝、并在报错里写着下一步怎么做
(file has not been read 一类占 49%),没有任何记忆能预防它;而分子需要一个语义判断
(“这条记录本该拦住这次失败吗”),本框架刻意不做模型调用——连词共现代理都会把“数据源独立性纪律”
算成“工具调用被中止”的相关记录,同一个病在上一层复发。继续追的唯一达标办法是把“改文件前先读”挂到
edit 上(占 28.3% 的调用,超触发预算 14 倍),那是作弊而不是覆盖。换成的新问题是能答的:
failure_shape 按形状计数并保留发生时间、delivery 记下送过什么、tools/prevention-ledger.mjs
出四分类账。实测与撤除依据见 docs/DELIVERY-GAPS.md 第五节
(含 2026-09-23 13:00 的撤除条)、第十五、二十节。
- 类型注解从不被检查。 构建只做剥离,toolchain 里没有 tsc(零构建依赖是刻意的),所以类型不一致不会被任何一步
发现——错注解被原样删掉,运行期行为不受影响,连测试都不会惊动。类型在这里是给人读的文档,不是被验证的契约。
要加门禁就得引入 TypeScript 依赖,与“构建期零依赖”冲突;这是明知的取舍,现在明确写在这里。
- 相关性闸会让“只共享功能词”的相关匹配落空。 常驻层要求命中标识符或共享一个实词,所以一句只含「这个/可以」这类词的
回话不会带出任何记录——即使某条记录确实相关。缓解手段是按需检索:memory_recall 不受这道闸约束。
- 标题比较折叠标点,所以同标题的不同主张可能被一起退役。 这是刻意的弱把手换来的:动作是退役而非删除,
supersededBy 与纠错日志都留痕,判断错了可以恢复。
- 那一行经验提示是每轮无条件付费的:204 字节,即使这个工作区永远不记任何东西也照付。这是有意的取舍——
把它做成“有记忆时才出现”会让它在库空时消失,而库空正是它要解决的问题。RECORD_HINT 的长度由测试钉了
256 字节上限;要彻底关掉它,删掉 src/index.ts 里的那次 ctx.systemPrompt.context 注册即可(它只贡献文本,
没有别的副作用)。
- 动手前把经验递到眼前,要求记录自己声明“适用哪次调用”(path: 文件名 / tool: 工具名 / command: 命令里的词,
写记忆时填 recall_for)。没声明的记录不会在动手前出现,只进每轮摘要、只被 memory_recall 搜到。
这是一次实测后的取舍:旧做法靠“调用与记录撞上同一个词”来猜,在 15,383 次真实调用上对 57% 的调用都发了提示,
抽 47 条人工看只有 5 条真的相关(10.6%);把门槛调紧到能去掉噪声,召回又掉到个位数。判据为什么改、实测数字、
人工抽查与失败路径,见 docs/DELIVERY-GAPS.md;node tools/anchors.mjs 能看当前库的覆盖率。
- 没有语义/向量检索。v1 只有 FTS5 + 标识符精确匹配 + 证据排序;record.embedding 列已预留,加入 RRF 融合时不需要迁移。
- 注入层是查询门控的,因此对话题漂移敏感。查询取自最近两条用户消息,所以用户回一句「继续」时,
查询层会清空。核心层(跨工作区印证过的领域级经验)正是为这个缺口存在的,但它只覆盖被印证过的内容,
工作区级的经验仍会在长任务中途续话时掉线。
- node:sqlite 仍是实验特性,运行时会打印 ExperimentalWarning。DSH 自己的会话全文检索也用它。
- 维护单轮最多 32 条,积压时不会自动提速。
- 导入不做跨库印证计数:迁移写入的记录 distinct_workspaces 恒为 1,领域晋升要等后续真实观察。
- 不提供图形面板;状态、预览与运维走斜杠命令,配置走插件 config。
- 斜杠命令需要 commands 服务。它由 dsh-base 提供——和 tools、systemPrompt 是同一个 bundle——
所以 inject 声明它并不新增环境约束。但由此推论:任何不含 dsh-base 的 profile 里本插件不会激活
(这在改动之前就已经成立,tools 与 systemPrompt 同样来自 base)。
- 随包不发 src/ 和 tools/。运行时只需要 lib/,而脚本是仓库内工具。这也消除了

「随包脚本 import src/.ts 因而在 node_modules 下跑不起来」那一类缺陷——不是修好它,而是不再发它。
- 不做跨机器同步;数据库是单机文件。
- 真实模型回合跑过三次独立实验,但它们都不在 pnpm verify 里。每一次都抓到了套件抓不到的东西:
1. 挂载层对不上真实 Session:两个读取器都在读 agent.session.events,而这个属性在真实
Session 上不存在——于是生产环境里事件日志恒为空,逐字引文永远定不到 verified-user,
检索查询永远是空串,注入层的查询段恒不命中。测试全部手写了那个数组,固化的是假设而不是
契约。现在读取统一走 src/session.ts(snapshotEvents(),其余为带标签的兼容分支)。
结论:挂载层断言替代不了一次真实回合。
2. 新旧两版投递判据的对照:无记忆 0/12 对 有记忆 9/12(p = 0.0003),跨会话 0/6 对 5/6
(p = 0.0152)。见上文「它到底有没有用」。
3. 换一类知识再复现一次:约定从"配置写什么"换成"文件放哪",无记忆 0/6 对 有记忆 6/6
(p = 0.0022)。同一节有合并数:两场景合计 0/24 对 20/24。

跑法见 docs/DEVELOPING.md 的「跑一次真实模型回合」与 tools/verified-user-ab/README.md,
但它们要消耗真实 token,所以没进自动化。
- 斜杠菜单的浏览器渲染没有自动化。命令的可发现性已经断言过了:测试用的是斜杠菜单读取的同一个 API
(ctx.commands.list(agent)),检查 7 个命令都在、都有描述、带参数的那几个都声明了参数提示、且按名排序。
剩下未验证的只是「浏览器把这份数据画出来」这一步——而这一步对 in-box 命令与本插件是同一条代码路径。

关于这份文档

当前版本 0.5.0(2026-09-25)。 完整版本记录(每一版改了什么、为什么改、实测数字)见
CHANGELOG.md;0.3.0 的关键变化是写入时把命中过宽的锚点丢掉并写明理由(外加把"这条经验有没有改变结果"记进 effect 字段,默认不参与排序);0.2.0 的关键变化是投递判据换成"记录自己声明 recall_for",
这是行为变更:没声明的记录不再在动手前打断工具调用。

- README 里的数字是机器核对的,不是手抄的。 tests/docs.test.ts 逐格比对配置表的默认值、注册的工具与命令名单、摘要行数上限(2+5=7)、测试套件数、审计产出清单,以及那一行提示的字节数;对不上测试就红。改文档和改代码是同一件事。
- 对外讲过的每一句硬话都登记在 docs/CLAIMS.json:一条一行,写明状态与凭证。measured 必须指向仓库里真实存在的检查或产物(tests/claims.test.ts 逐个确认文件在不在);没跑的东西只能写 not-run,而且不许带凭证;已经讲出去、但仓库里没有可复跑凭证的,如实登记成 readme-only——那是待补的债,不是合格状态。
- 测试按标题逐字定位。 被钉住的标题是 ## Known Limitations and Deferred Work、## 配置、## 模型的体验(Model Experience)、### 它挂了四个表面、#### Token effect——重命名它们要同时改测试,否则整套检查会找不到锚点而失败(失败,不是静默跳过)。细节见 docs/DEVELOPING.md 的「文档与代码对齐」。
- 一共 23 个套件,一条命令跑完全部:pnpm verify。各套件覆盖什么,见 docs/DEVELOPING.md 的「测试」。
- 构建、打包、启动验收、测试清单与开发环境在 docs/DEVELOPING.md。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入社群

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

DPharness QQ 群二维码,QQ 扫码进群
QQ 扫码进群
DPharness 飞书群二维码,飞书扫码进群
飞书扫码进群