dsh-jev-memory
类型化、可审计的 DSH 长期记忆插件。 一句话定位:
AGENTS.md是人手写的记忆;这个插件是自己长出来、按需召回、逐条可审计的记忆。
DSH 官方有会话持久化(JSONL)、会话检索(SQLite FTS)、compaction(压缩丢弃),但跨会话的长期记忆是空白:没有任何官方包回答"什么值得记住"和"这一轮该想起什么"。本插件补的就是这一层判断。
它做什么(当前已实现并实测)
| 环节 | 实现 | 位置 |
|---|---|---|
| 写入 | 挂在 agent/turn-stopping(serial + awaited,回合收尾前)抽取候选 → 判定 → 过确定性闸门 → 落盘 |
dsh/index.ts |
| 判定 | 端口式:Jev(TypeSafe 判定模型)为首选,确定性信号为离线兜底。判定层只出标签与概率,不生成文本 | dsh/lib/judge.ts、dsh/lib/jev.ts |
| 存储 | 本地 memory.json + 只追加的 ledger.jsonl,原子写、内存索引(注入回调必须同步) |
dsh/lib/store.ts |
| 召回 | systemPrompt.context 每步注入当前工作区的 Top-K,带类型/id/日期标签 |
dsh/lib/recall.ts |
| 工具 | memory_search / memory_write / memory_forget,模型可主动查、主动记、主动撤 |
dsh/index.ts |
三条纪律写进了代码,不是写在文档里:
- 模型不写——记忆正文只能是用户原话(逐字裁剪)或工具失败的确定性签名,判定层没有可以塞进新句子的字段;
- 概率只排序,阈值归代码——写入阈值是
minImportance,冲突判断用插件自己的conflictThreshold,从不把 Noul 上调好的阈值搬到 Choice 上(官方 jaggedness 文档:P(q)+P(¬q) 未必等于 1); - fail-open——判定失败降级到启发式,钩子超时直接放弃,写入路径的任何异常都被 catch,绝不让记忆成为会话失败的原因。
快速开始
方式一:开发期直接挂工作区(无需安装)
在 profile 的用户 patch 层 ~/.dsh/profiles/web/cordis.patch.yml 里加一行:
- insert:
- id: jev-memory
name: 'file:///absolute/path/to/dsh-jev-memory/dsh/index.ts'
行名由加载器用 new URL(name, baseUrl) 解析,所以 file: URL 与裸包名都可用。删掉这一行即完全卸载。
方式二:作为一个包安装进 profile
dsh plugin --profile web add /absolute/path/to/dsh-jev-memory
# 再把包名加进 profile package.json 的 dsh.profile.bundles
包根自带 cordis.patch.yml,dsh.bundle.patch 指向它。
配置
- insert:
- id: jev-memory
name: 'file:///absolute/path/to/dsh-jev-memory/dsh/index.ts'
config:
types: [constraint, pitfall, decision] # 只记这三类
judge: auto # auto | jev | heuristic | off
minImportance: 0.6 # 确定性写入阈值
minRemember: 0.6 # Jev 的"值得记吗"阈值
askOnConflict: true # 发现矛盾时问你一句(HITL)
askOnConflictTimeoutMs: 600000 # 等你回答的预算(10 分钟)
askOnConflictMaxAttempts: 3 # 超时后在下一次对话开始前重问,最多几次
repeatFailuresToWrite: 2 # 同一个错重复几次才算"坑"
writeSkipSubagents: true # 见下方"实测发现"
writeTimeoutMs: 2500 # 回合收尾的写入预算,超时放弃
recall:
maxTokens: 600
quota: { constraint: 4, pitfall: 3, decision: 2 }
jev:
apiKeyEnv: TYPESAFE_API_KEY # 从下面三处按序解析,密钥不写在配置里
model: jev-latest # 台账会记录实际应答的版本号,便于发现别名漂移
密钥放哪:按序解析 ① 宿主 ctx.credentials 服务的 apiKeyEnv 引用(存 ~/.dsh/.credentials.yaml,权限 0600)→ ② 直接读同一个凭据文档(服务在开机那一瞬可能还没就绪,这条兜底让"时机"不再决定判定方式)→ ③ 配置里的 apiKey / 进程环境变量。解析发生在每次判定时而不是挂载时,所以运行中新增或轮换 key,下一轮就生效、不必重启。台账的 start 行写明实际用了哪条路径(service:file / file / config / env / none)。没有 key 时 judge: auto 自动退回启发式,插件无需网络即可工作。
记忆互相矛盾时:问你一句(人机协作)
"这条值不值得记"是一回事,"它是不是和你之前说的冲突"是另一回事——后者机器不该自己拍板:
- Jev 若判存在冲突(概率 ≥ 0.7)且这条本身值得记,先以"待确认"状态落盘(不注入、也不丢);
- 回合结束时问你一句——一条问题、三个选项:用新的覆盖旧的 / 保留旧的那条 / 两条都留着;
- 你的选择决定结果:覆盖 → 旧的标成"已被取代"(留档,不再注入);保留旧 → 新的丢弃;都留 → 两条都生效;
- 没人应答 / 超时(默认 10 分钟)/ 出错 → 保持"待确认":宁可少记一条,也不猜你的意思。超时不是终点:记录留在"待确认",在你下一次发消息、模型开始干活之前再问一次(那时的你一定在场),最多重问 3 次——这就是"超时敢设长"之所以安全的原因。
为什么这和 DSH 原有的 HITL 不冲突:DSH 有两种跟人打交道的通道——审批(沙箱/权限策略发起,"这个危险操作放不放行")和提问(模型发起,"我缺一个信息")。这里复用第二种,只是换成插件的判定规则来发起:不新增 UI、不新增权限模型、不新增打断渠道,问题会出现在你熟悉的那个提问卡片里。唯一的差别是"谁决定要问"。
台账会记 conflict-ask(问了什么、和哪条冲突、配对依据的共享词)与 conflict-resolved(你选了什么)——这就是"自动判断靠不靠谱"的实测数据。
一次性失败不记:工具报错要同签名重复出现(默认 2 次)才算"坑"。一次性的环境失败(沙箱拒绝、重定向被拦)不是教训;重复发生的才是。次数从台账里数,所以重启不丢。
实测发现:写入精确率 1/4(已修)
第一次在真实 GUI 里跑,台账 ledger.jsonl 立刻暴露了问题:
{"kind":"write","type":"constraint","by":"heuristic","importance":0.96,
"source":{"sessionId":"5883ea6a-…","seq":8,
"quote":"调用 `memory_write`,参数:{\"text\": \"语言不要选 Java\", …} 3."},
"signals":["/不要/u"]}
这些是我写给 subagent 的任务指令,被当成"硬约束"记了下来。根因不是阈值调错,而是:
在委派子会话里,
source.kind === 'user'的消息不是人类的话,是父代理的指令。
核实方式是从磁盘读那条会话的 header:{"delegationDepth":1,"agentPreset":"cordis"} —— 确认是子会话。
修复分两层,都带回归测试(测试用例直接用上面这些真实句子):
writeSkipSubagents: true(默认开)——深度 >0 的会话不再参与学习。子会话的回合对父会话的钩子同样可见,所以不丢信息;screenSentence()——拒收任务指令(按下面步骤、第 N 步、把…贴出来、不要改写…)与载荷({"key":、代码块)。刻意做窄:不要改动 data/ 目录这类真约束必须活下来。
这条记录本身就是这个插件的价值主张:误记是可以被逐条追责的,因为每条记忆都带着来源会话、seq、原句和命中的信号。
指标(都能从台账算出来,不是估计)
| 指标 | 怎么算 | 当前 |
|---|---|---|
| 写入精确率 / 误记率 | eval/report.ts 在人工标注的真实语料上算(当前 38 句已标,逐句判断见 eval/labels/README.md);eval/write-precision.ts 是 20 条的冒烟集 |
只过筛子 43% / 100%;离线规则判定 50% / 89%(F1 0.64);Jev 50% / 11%(F1 0.18)(精度 / 召回,实测于 2026-09-29) |
| Hit@K | 对每条标注查询 Q:前 K 条里只要有 1 条标注正例即记 1,最终 = 命中查询数 ÷ 查询数 | 待测 |
| Recall@K | 对每条 Q:前 K 条里的正例数 ÷ 该 Q 的全部正例数,再取平均 | 待测 |
| 注入 token 数 | 台账 kind:"recall" 的 tokens 字段 |
1 条约 60 tokens(历史上 4 条时 289–360) |
| 被拦下的候选 | 台账 kind:"skip" 的 reason |
veto:secret / veto:task-instruction / veto:payload / duplicate / subagent-session / below-min-remember / below-min-importance / type-disabled:* |
| 判定降级 | 台账 kind:"degraded" |
jev-missing-row(候选超出单次请求上限时静默降级,现在会记账并告警) |
写入精确率是怎么量的
eval/write-precision.ts 是一套带标注的候选集(20 条 = 8 条该记 + 12 条不该记),其中 8 条"不该记"取自真实会话里插件确实写过的原句(子代理任务指令、一次性环境失败、用户提问、API key 本身)。
它只有 20 条,只能证明"没有明显坏掉",不能当结论用。 真实分布的量法在 eval/labels/:从历史会话里抽出候选句、由人逐句标注,再对照三种判定(只过筛子 / 规则判定 / Jev)。那份数据的抽样口径和批次说明写在 eval/labels/README.md,逐句结果在 eval/labels/report.md(node eval/report.ts 生成)。
真实语料上的结果:结论取决于那 8 行标注
在 38 句人工标注的真实句子上(编码类 9 句已定),三种判定是这样:
| 判定 | 精确率 | 召回率 | F1 |
|---|---|---|---|
| 只过筛子(当前规则) | 43% | 100% | 0.60 |
| 离线规则判定 | 50% | 89% | 0.64 |
Jev 判定(remember ≥ 0.6) |
50% | 0–11% | ≤0.18 |
Jev 在 9 条正例里只写了 0–1 条(重复跑会在 0% 和 11% 之间跳),而它把这些句子的类型全判对了(constraint / decision),只是 remember 只给 0.07–0.59 分。但先别下结论:这 8 条被漏记的句子全是祈使句(请你按照这个格式去写简历、改成"一条消息 → 一串任务"、明天晚上有一个淘天的面试…帮我复习),而标注标准里写着"一次性任务指令 = 0"。标注者本人也提出"有些内容确实不需要沉淀"。
所以结论对这几行标注高度敏感(V0–V3 是逐步把它们改标 0):
| 场景 | 正例数 | Jev 精确/召回 | 离线规则判定 精确/召回 |
|---|---|---|---|
| 按当前标注 | 9 | 50% / 0–11% | 50% / 89% |
| 3 条明显的一次性指令改标 0 | 6 | 50% / 0% | 31% / 83% |
| 5 条可疑的改标 0 | 4 | 50% / 0% | 19% / 75% |
| 只留最明确的一条(语言不要选 java) | 1 | 50% / 0% | 6% / 100% |
也就是说:若那 8 条本来就该标 0,那么有问题的是离线规则判定——它会把一次性任务指令全部写进记忆,精确率掉到 6%;Jev 的拒绝反而是对的。在这批标注重判之前,两个判定谁更好没有结论。
冲突解决不再真删旧记忆(2026-09-29)
replace 那条分支调用的是 store.remove(existing) —— 旧记忆被真删,而它上方三行的注释写着"用 superseded 而不是删除:旧记录保留可审计",给用户的选项说明也写着"保留记录,供以后查证"。也就是代码在否定自己的注释,并且对用户说了假话。数据模型其实早就支持 superseded(store.ts 的类型里有,normalizeStatus 也认),只是没人用它。
现在:旧记录保留原文、置 status='superseded' + supersededBy,新记录写 supersedes;召回与 memory_search 都跳过 superseded(历史不是当前知识,但不许悄悄消失)。memory_forget 仍然真删 —— 那是用户明确要求删除,留副本会是另一种假话。
片段筛子:只上量过的那几条(2026-09-29)
我原本打算把"以 \end{itemize} 开头""含 /var/folders/... 临时路径"的句子直接筛掉。先量代价,结果否掉了这个方案:这两类句子在你标注里都是该记(标 1) —— 它们是"内容该留、前缀该脏"的 keeper,筛掉它们等于毁掉插件存在的理由。所以最终只上线了逐条测过、零误杀的四条:
| 规则 | 命中已标注行 | 其中标"该记" |
|---|---|---|
行首是代码注释 //… |
3 | 0 |
行尾分号 ; |
3 | 0 |
行首是代码关键字(if/class/public…) |
1 | 0 |
行首是 markdown 结构残留(**/---/#/` |
`) | 1 |
那两类 keeper 的正确修法是清洗前缀而不是丢弃 —— 已经做了,关键在于身份与展示分离:
id(句子的身份证)永远按"你写的原句"算,所以清洗不会改变任何已标注行的 id,你的标注照旧有效;text(存下来、注入进上下文的那份)是清洗后的:去掉行首的\end{itemize}/\item/\textbf{...},把临时路径(/var/folders/、/tmp/、粘贴暂存目录)换成签名里已在用的<path>占位符。通用路径不动(配置文件路径可能就是一条记忆的全部意义)。- 筛子改为跑在清洗后的文本上,顺带修掉一个隐藏 bug:前缀曾经能改变判定 ——
请你按照这个格式去写简历…本来就会被"一次性任务指令"筛子挡下,是前面的\end{itemize}把它"救"成了候选。
这个"身份取自原句、其余取自清洗文本"的错位很容易写反,我第一版就写反了(注释说原句、代码传了清洗后的),是测试抓出来的,现在有测试钉住。
两层记忆:原句是证据,规范形式只用于注入(2026-09-29)
之前存的是你原封不动的那句话。好处是"绝不编造"——记忆永远能追回到一句你真的说过的话;坏处也真实:错别字、口水话、断句会一起进上下文。别家都改写(memsearch 把每回合变成 2–10 条第三人称 bullet,OpenViking 抽进带 as of 日期的类型化文件),照搬原句的才是异类。
所以现在是两层,分开的理由就是这张表:
| 内容 | 谁写 | 用途 | |
|---|---|---|---|
| 证据层 | 你原句,永不被模型改写 | 你 | 台账、source.quote、逐条核对 |
规范层(新增字段 canonical) |
模型整理过的第三人称陈述 | 模型,受硬约束 | 注入(默认不启用) |
三道闸门防止它变成编造通道:
- 不许添加事实 —— prompt 明说,并且有确定性闸门兜底:规范形式的词元里必须有足够比例来自原句。阈值不是拍的,是量出来的:把「嗯那个啥 就是 端口别乱改啊 定了 8000」整理成「服务端口固定为 8000,不要修改。」共享 0.375 的词元,而一句编造了理由和后续动作的扩写只共享 0.19,阈值取 0.25。判错的方向是刻意选的:拒了只损失可读性,收了就把编造的内容送进上下文。
- 不许静默接受 —— 过不了闸门不修补、不裁剪,直接丢掉、保留原句。fail-open 在这里的含义是"保留原话",不是"保留模型猜的"。
- 没有路由就不做 —— Jev 根本做不了这件事(它只能回答
noul/choice/score,输出不了文本),所以走宿主自己的llm服务,路由取自agentDefaultModel.currentSelection(),也可以配置覆盖。没有路由就什么都不做,并且只在挂载时报告一次(start台账行的normalize字段),不会每写一条就留一行失败噪音。
配置与默认值:
config:
normalize:
enabled: true # 生成规范形式(默认开)
inject: false # 注入时用它替换原句(默认关 —— 这会改变模型看到的内容,先看样例)
# provider / model 留空 = 用宿主的默认模型
默认不注入,因为这一步改变的是模型看到什么。想看样例再决定的话,台账里的 kind:"normalize" 行就带着 from / to(各 160 字)—— 打开 enabled 跑几天,读那些行,满意了再把 inject 打开。
冲突窗口的排序:默认 BM25,可选 embedding(2026-09-29)
冲突确认那一步不会把全部记忆丢给模型——只给 knownForConflict 条(默认 20)。原来这 20 条是"存储顺序的前 20 条",于是排在第二十一条的矛盾永远不会被看到,模型回答"不冲突",台账里什么都不留。现在改成按相关性排序,打分函数是可替换的接口:
conflictRanking |
实现 | 依赖 | 代价 |
|---|---|---|---|
lexical(默认) |
对记忆做 BM25 | 无 | 无,且没有任何内容离开本机 |
embedding |
余弦相似度(智谱 embedding-3) |
一个 key | 记忆正文会发给该服务商;派生向量缓存在 embeddings.json |
BM25 用在这里合理吗?合理,而且中文分词的问题已经解决了 —— 不需要任何依赖。 我原来写"中文没有分词器",这个说法是错的:Intl.Segmenter 是 V8 自带的,Node 又是完整 ICU,中文词典分词本来就在机器上。
实测(查询 端口不要用 9000 了,正确结果应是"服务端口用 8000"排在"不要用 yarn"前面):
| 分词方案 | 不要用 yarn | 服务端口用 8000 | 结论 |
|---|---|---|---|
| 全量字符 bigram(原来) | 2.733 | 2.188 | ❌ 排错 |
Intl.Segmenter 词元(现在) |
1.814 | 4.959 | ✅ 正确 |
| 分词器词元 + 词内 bigram | 1.814 | 4.959 | ✅ 正确(与上等价,取更简单的) |
bigram 输的原因是 不要用 被切成 不要 + 要用,而 要用 成了罕见词、凭空制造 IDF;分词器在 不要|用 处切开,这个假词就不存在了。代码里还加了运行时自检:精简 ICU 的构建会把中文切成单字(比 bigram 更差),所以加载时用一句话探测,不通过就退回 bigram。
BM25 真正修不了的是改写(data 下的文件别碰 ↔ 不要改动 data/ 目录 只共享一个词),那是下面 embedding 开关存在的理由。
embedding 修的是另一种情况:改写。 实测(真实 API 调用,2026-09-29):
| 对比 | 余弦 |
|---|---|
不要改动 data/ 目录下的任何文件。 ↔ data 下的文件别碰(改写) |
0.769 |
不要改动 data/ 目录下的任何文件。 ↔ 服务端口固定 8000(无关) |
0.532 |
无关句也有 0.53 —— 所以它只能用来排序候选,不能当绝对阈值:真正区分的是相对顺序。另一方面 3 条文本一次请求 305ms、0.5 元/百万 token(全库约 3 万 token ≈ 0.015 元),缓存命中后不再请求。BM25 对这一对改写的得分接近 0,这也是为什么这个开关值得存在。
失败一律回退到 BM25(无 key、超时、报错、维度不一致),因为排序是优化,写入不能依赖它。
问法改了一版,A/B 量过(2026-09-29)
原来那句判定说明里写着"是用户对项目的说法或偏好(不是这一次任务的操作指令)"——它让模型去判句子的形式,于是祈使句一律被否,连"我要求你说设计…就说流程就行了"这种长期偏好也被当成一次性指令(0.14 分)。现在改成判内容的时效:"以后新的会话里是否仍然适用或成立……句子是以指令的形式说的并不影响判断"。
同一批 38 条标注、各跑 3 次:
| 问法 | Jev 精确率 | Jev 召回率 | 三次跑的结果 | 最明确那条约束 |
|---|---|---|---|---|
| 旧(判句子形式) | 0% | 0% | 0%/0%、100%/0%、0%/0% | 漏记(0.59) |
| 新(判内容时效) | 33% | 11% | 33%/11% 三次一致 | 记下了 |
改动是真实的,但很小:召回 0% → 11%,代价是多放 1 条(一段粘贴文本里的"这个一定要记住"被当成了你的要求)。剩下没救回来的仍是"祈使式的项目偏好"(是一个风格和精简程度…、改成"一条消息 → 一串任务")。
新问法下阈值扫描也好看得多(0.20 时 42% / 56%,旧问法同阈值只有 50% / 33%),但没有动默认阈值 0.6:30 行已定标注不够定阈值,而且分数排序并不单调(0.4 那档比 0.5 还差)。
没有结论的部分:如上面的敏感度表,两个判定谁更好仍取决于那几行标注;但"祈使式项目偏好会被 remember 拒掉"这件事,在两种问法下都成立。
TYPESAFE_API_KEY=... node eval/write-precision.ts # 无 key 时只跑确定性筛查 + 启发式
TYPESAFE_API_KEY=... WP_REPEAT=10 node eval/write-precision.ts # 模型判定跑 10 次,看分数波动
实测于 2026-09-29(jev-1.13.0,20 条里确定性筛掉 4 条、16 条进判定,单次 1.3–3.4s):
确定性筛查:拦掉 4 条(2 条任务指令 + 1 条提问 + 1 条密钥),正例 0 损失
启发式 precision=0.86 recall=0.75 F1=0.80
Jev @0.3 precision=0.89 recall=1.00 F1=0.94
Jev @0.5 precision=0.88 recall=0.88 F1=0.88
Jev @0.6 precision=1.00 recall=0.75 F1=0.86 ← 默认阈值(同一次跑的另一次运行给 0.86,见下)
Jev @0.7 precision=1.00 recall=0.63 F1=0.77
两个必须一起读的事实(否则上面这张表会误导):
- 模型判定不是确定性的。 同一批输入重复跑 10 次,16 条候选里 15 条的分数都在变(最大波动 0.07),其中 1 条的分数在 0.54–0.60 之间反复越过阈值——单次运行会让精确率在 0.86 和 1.00 之间跳。所以 Jev 的数字必须报重复跑的区间,单次结果不能当结论。
- 默认阈值 0.6 会稳定地漏掉中文项目约束。
不要改动 data/ 目录下的任何文件(硬约束,标注为"该记")10 次跑下来是 0.50–0.53,sqlite 写入失败:EDQUOT…(可复现的坑)是 0.34–0.41——两条都稳定低于阈值。这不是噪声,是召回损失:@0.6 的召回 0.75 就是漏了这两条。
写进台账的判定分(remember)现在也可以直接从台账复核:kind:"remember" 记录里带 by 和 model,能看出某条记忆是 Jev 判的还是启发式判的。
默认取 0.6 而不是 F1 更高的 0.3:假阳性比假阴性贵——一条错记会进入此后每一个会话的提示。两个已知漏判也写在这里:不要改动 data/ 目录下的任何文件。(Jev 给 0.51,恰好压线)与 sqlite 写入失败:EDQUOT...(给 0.38——只看到一次,无从判断会不会复现;这条已由确定性规则补上:同签名重复 ≥ repeatFailuresToWrite 次才落盘)。
为什么分开写 Hit@K 和 Recall@K:多正例下两者会分叉(Hit@5=1 时 Recall@5 可能只有 1/3),只写"Top-3 命中"是不可复现的。这个教训来自
zilliztech/memsearch公开的中英检索评测——它同时发布了两套定义,参见docs/memsearch-notes.md。指标分工也定了:Jev 只用在写入判定,不用做召回重排。memsearch 的公开评测里 Jev 作为 reranker 输给 Voyage rerank-3(Recall@5 0.7941 vs 0.8187、MRR@10 0.6884 vs 0.7754),作者据此拒绝把 Jev 设为默认——那是"给已有候选排序"这个位置上的负结果,和我们"写入判定"不是同一个任务,但任何对外表述都必须承认这条公开数据。
- 两种投递方式:正常情况下走系统提示里的运行时上下文(便宜、稳定);检测到 preset 关掉了运行时上下文时,改走一条带插件来源的消息(
form: "recall"),并在台账记context-suppressed。这条兜底是必需的:那种情况下宿主的装配器根本不会调用我们的回调,插件既不报错也不留痕——完全静默。
明确不做(避免和生态重复)
- 不做 UI / 面板、不做向量库(类型化 + 少量记录不需要);
- 不做"从反复失败里长规则"(那是
dsh-jev-forge的题目)、不做全量写入的人工分诊——只在你和旧记忆真的矛盾时才打扰你(见上一节); - 不碰
dsh-jev-tools已覆盖的四个节点:工具输出精简、注入筛查、技能推荐、交付闸门; - 不与
AGENTS.md争优先级:人工手写 > 自动记忆;冲突条目先落needs-review且默认不注入,再由你决定归属。
测试与验证
node --test test/*.test.ts # 80 条,全离线,不联网、不启动 harness
pnpm run typecheck # tsc --noEmit,零依赖包也能有真类型检查
包含一条完整的端到端闭环(用假 ctx 驱动真实代码):写入 → 下一会话召回 → memory_search → memory_forget。
为什么是 TypeScript 而不用构建:Node 22.23 默认开启类型擦除(process.features.typescript === 'strip'),所以 import('./dsh/index.ts') 与 node --test test/*.test.ts 都能直接跑,没有构建步骤,也就保住了"零运行时依赖 + 可被 file: URL 直接挂载"这个性质。代价是只能用可擦除语法(无 enum / namespace / 参数属性 / 装饰器),相对导入必须写显式 .ts 扩展名。类型检查靠 devDependency 里的 typescript(与运行时无关)。
真实 harness 验证已做过一次(记录在 docs/DESIGN.md 的"实测记录"一节):插件热挂进正在运行的 web profile,台账出现 start,一次子会话回合触发了 4 条写入,下一轮模型请求的系统提示里出现了 ## 长期记忆 区块。
现状与未验证项(不编造)
- 离线全绿、真实进程内闭环已验证;
- Jev 线上路径已用真实 key 跑通:
POST https://api.typesafe.ai/v1/systemone→jev-1.13.0,docs/jev-api.md里的请求/响应结构逐字段对上(含answers.<id>.noul / .choice / .score / .confidence)。延迟量级 0.56–0.68s,但那是一次判 16 条、把上限临时调到 50 测出来的;默认上限是 6 条(第 7 条起退回启发式,现在会记kind:"degraded"并告警); - live 判定已确认走 Jev(2026-09-28 重启后核实):台账
start行写着"version":"0.6.0"、"jev":{"ready":true,"source":"service:file"},且此后的写入/跳过记录都带"by":"jev"、"model":"jev-1.13.0"。凭据走两条路(宿主凭据服务优先、直读凭据文档兜底),所以"服务在开机那一瞬还没就绪"不再决定判定方式; - 多进程同时写同一个
memory.json是 last-write-wins,单进程是受支持场景(TODO:锁文件); - 代码改动必须重启 harness:Cordis 的 HMR 能重挂载插件行、重读存储,但 ESM 缓存不会重新求值
lib/*.ts。这条是实测的,不是推测:一次语义变更触发的重挂载后,start台账仍写着旧version、也没有新字段。start.version就是用来判断"跑的是哪份代码"的;换一个文件路径(如.js→.ts)会强制换一套模块 URL,那是唯一不用重启的刷新方式; - 同机还有一个同类插件
@zilliz/memsearch-dsh(见docs/memsearch-notes.md):它与本插件正交,但挂在同一 patch 层,同 profile 共存未实测(可能双份注入)。
目录
dsh/index.ts 插件入口:钩子、注入点、三个工具,以及本地结构类型
dsh/lib/signals.ts 确定性信号词表 + 候选筛查(含密钥/任务指令/载荷否决)
dsh/lib/extract.ts 从回合事件里抽候选
dsh/lib/judge.ts 判定端口(Jev / 启发式)+ 确定性写入闸门
dsh/lib/jev.ts Jev HTTP 客户端(凭据解析、重试、超时、解析)
dsh/lib/store.ts memory.json + ledger.jsonl
dsh/lib/recall.ts 选哪些记忆、怎么渲染
dsh/lib/conflict.ts 冲突配对(词重叠)+ 三选项卡片 + 答案映射
dsh/lib/credentials.ts 直读凭据文档的兜底路径(极小的 refs 段解析器)
dsh/lib/text.ts 文本工具(分句、估算 token、哈希)
test/ 80 条离线测试(plugin.test.ts 用假宿主驱动完整闭环)
eval/write-precision.ts 带标注的写入精确率评测(可对标、可复现)
tsconfig.json noEmit + allowImportingTsExtensions(Node 擦除模式可直接跑)
docs/jev-api.md Jev 调用契约调研(带出处链接)
docs/memsearch-notes.md 同类项目 memsearch 的源码级调研与逐项对比
docs/DESIGN.md 设计决策与实测记录
No comments yet. Be the first to write one.