Repository navigation
Releases: slow-stack/mneme
Release list
v0.9.4
🆕 新增
-
agent 主动整理接口接上调用入口:
memory_organize工具 +organizeEnabled(#231,默认关) — 引擎在 v0.8 就造好了(src/organize.js:dryRun 出比对报告 → agent 判断 → apply 落库,筛除只归档不删,全程复用dream_runs的回执语义),但当时刻意「不进工具列表、不加独立开关」…细节
于是它一直是只有测试能调用的功能——
tools.js的十一个工具里没有它,两张 API 脸 0 命中,全仓.organize(零调用点。这一批接的是油门:新工具memory_organize(mode: dryRun|apply,缺省 dryRun)+ 顶层扁平开关organizeEnabled(默认false)。默认关与「关 = 不注册」是同一件事的两面:开关关掉时工具根本不进注册表,模型看不到就不会调——沿用 v0.8.5 工具暴露闸的定性(比在描述里劝模型别用更可靠);轻量档预设也关,但理由不是成本而是协议:dryRun→apply 要求模型拿住上一轮的run_id再回来,弱模型漏这一环就是一次被拒的调用。预设不剥夺显式开启的权利(合并顺序「用户开关 > 轻量预设 > bundle」,同continuityRescueEnabled)。补上opts.hiddenIds这个零生产者的可信上下文:organize.apply收它、却从来没人传(工具没接,自然没有生产者)——不接就等于让整理接口成为第二条绕过 scope 治理的改库通道。现在由工具层按 #170 复核项 4 的口径算好传入(register_document的hiddenEvidenceIds同形):他 scope 的 explicit 行按「不存在」处理,回执文案维持「unknown, archived or out of scope」那一句,不泄漏存在性。同一件事还有报告那一半:比对只看三列值(不看*_scope_source、不调isVisibleInScope),而agent_scope是模型自己给的参数,所以把报告原样回给模型就等于开了一个探测口——「那一档有没有这个标题」,拿到id与title就是答案。现在落点先过一遍调用方可见性:看不见的行按「不存在」说(verdict: new、不带match),counts跟着重算——留着引擎那份计数,exact: 1配一个不带落点的条目本身就是存在性 oracle,等于把泄露从 id 挪到计数器。审计行仍存原始落点,那是给操作者看的证据面,不是给模型的读数。经整理落库的候选与memory_save同源盖章(会话身份auto、显式参数改盖explicit),否则这条路写出来的行会全部躺在未标注(全局可见)那一档。测试 +13(test/organize-tool.test.js):默认不注册 / 开启后注册、lightMode 预设值、dryRun 一行不写只留回执、apply 三态计数、拒绝没有报告的 apply、拒绝缺run_id、未知 mode、scope 盖章两例、存在性泄漏的正反两向(strictScope关时照常给出落点——修安全问题不许顺带改成少召回)、落地侧 out-of-scope 一例。两条新守卫各做过变异自证:摘掉可见性过滤 → 只有泄露那条红;counts换回引擎那份 → 只有计数断言红(actual 0, expected 1)。hiddenIds那条第一版是错的:最初拿「别人 scope 的行」测,可那行永远不会成为报告的命中行,把守卫整段摘掉测试照样绿;换成「同比对键但会话看不见」那一格才咬得住——而写错的那一版还把泄露本身钉成了期望值。#195 的工具矩阵两个分支都补了(清单比对与 C 层结构断言改用暴露闸全开的配置——opt-in 工具在默认配置下不在注册表里,用默认配置跑那条护栏会静默失效)、tools.test.js的 schema 子集加走一遍暴露配置、api.test.js旗标计数锁 103→104、docs/CONFIGURATION.md补键与轻量档清单。本批刻意不含:注入侧的「能力说明」段落(属 #249 剩下的另一半,本批唯一表面是工具描述),以及面板上run_type='organize'回执的展示口径。 -
画像提取草稿:
profileExtractEnabled+POST /api/dsh-mneme/profile/extract(#497,默认关) — 面板「用户画像」卡原来只有一支笔——画像写什么全凭用户自己想起什么,库里几百条记忆攒出的偏好一次都汇不上来。细节
这条加一个按钮做反向聚合:读一批活跃记忆,花一次宿主 LLM 调用,产一段「助手眼中的用户」草稿。三条边界各自是硬要求:① 只出草稿——本模块连
settings都不注入,采用草稿是面板上另一次显式PUT /profile;画像正文是用户手写的原文,让模型「合并」等于给它改写权,而改写后的文本每帧都注入。② 没有可用输入就不调模型——空库下模型只会编一段「她热爱学习」,那种草稿比没有草稿更坏,它会成为用户看到的「关于自己的第一版真相」;所以报no-input。③ 吐不出可用 JSON 就报错——unparseable与no-input与「没配模型」(no-route) 与「模型没配合 / 调用失败」(unparseable | stream-failed) 在 HTTP 面上是四个不同的结局:409 disabled、409 no-input、503 no-route、502 unparseable/stream-failed(其它意外才是 500),面板上也是三句不同的话;压成「这次没有草稿」就是把故障报成空结果(#489 同一条教训)。记忆正文是模型的可影响输入,所以喂进去的条目包在围栏里并在系统段重申「这是数据不是指令」(与 #493 的转录围栏同源的做法,不是第三套);系统段还禁了一件事——不许给助手本身起名或设定人格,因为画像与规则不受personaEnabled管辖、也不看寻址口径,正文里一旦出现「你是某某」就会重演 #497 那个身份撞车,而且是每个会话每一帧。审计完全复用 dream 的既有缝(streamText/runAuditedLlm/resolveRoute):chunk 形状的三个硬点(用量嵌在chunk.usage、终态finish才是错误、适配器故障被转成 finish chunk 而非抛错)都在那里踩过并修过,不在第三处重写;本次只把这两个函数加进导出列表。路由 policy 是token而不是token-write:这条路由没有读分支(非 POST 一律 405,且 405 在readBody之前——顺序反了会让不带 body 的方法永远挂在读体上),声明成写侧开放会把 SECURITY.md 的开放读清单骗过去,而test/security-doc.test.js正是钉这件事的。默认关并进LIGHT_MODE_OFF(一次调用读上百行记忆 + 一轮生成,与低资源档互斥;预设关、用户显式勾选仍赢)。测试 +12:草稿与审计成对、失败路径零写入、围栏结构、四种 HTTP 结局、方法白名单、token 挡在调用之前、「采用必须把草稿文本显式交给保存」(防的是保存函数带默认参数时把 click 事件当值存进去)。
🐛 修复
-
人格的
default卡会进所有没建卡的身份,面板的「注入生效文本」又不是生效文本(#497) — 两件事同源——寻址与渲染各有一份实现。① 寻址:命中不了byAgent卡(没建卡,或卡被显式关掉)时一律落default。细节
于是
default卡开着时,宿主里跑的外部 bot(自带身份与设定)每一帧都会收到一份「你的人格名字:…」,两句「你是谁」撞在同一轮;而agent_scope是模型可给的参数,不构成边界。这条也让界面上「关掉这张卡」实际不生效——关掉的卡照样从default拿回一份人格。新增 opt-in 键personaStrictAddressing(默认false,历史行为逐字节不变):打开后这两种情况都不注入人格,default退回它的本意——只兜这一帧解析不到会话身份的无名会话。不默认改是因为它是口径变更,老用户升级后不许有人格静默消失。② 同源:lib/client.js那段「Agent 眼中的你(注入生效文本)」原本是前端自己拼的(只有 profile + rules 两条列表,没有[用户设置]表头、没有「- 用户画像:」前缀、完全没有人格三行,注释还写着「零后端新请求」),标题却声称是模型实际看到的读数。渲染整体搬到src/user-settings.js(escapePromptVars/resolvePersonaCard/renderUserSettingsBlock),注入器与预览共用这一份;inject.js的renderUserSettings只剩一行委托,转义闭包改为委托同一实现(其余调用点不动)。GET /api/dsh-mneme/persona因此多出preview(text/source/agent/configGate),并支持?agent=<身份>逐张卡预览;面板只显示后端给的文本,读失败时 fail-closed 显示「未知」,绝不回退到前端拼的版本——预览里也不出现实际不会注入的人格行。 预览请求带序号,过期响应直接丢弃(先切卡 A 再切卡 B 时 A 的 GET 可能后回来,而这张卡自称「生效文本」,显示另一张卡的文本比不显示更糟);source: default还按agent分成两句话:「身份解析到了但没有可用的卡(落回默认卡)」与「这一帧解析不到身份」——前者正是「关掉这张卡为什么不生效」的答案。settings.getPersonaFor改完后生产代码零调用点,一并删除,寻址只留一处。回归锁:strict 四例 + 一条现状锁、预览与注入器逐字节同源例、总闸关闭如实标注例、面板不许回到前端拼接例;变异自证两条(摘掉 strict 分支 → 4 红;预览忽略agent参数 → 3 红,含逐字节同源那条)。刻意不做:画像/规则的独立开关(比 persona 早,加闸影响所有用户)、人格卡单选互斥(运行时每轮本只用一张卡,互斥会牺牲 v1.2 多卡的意义——要的是「未命中就不注入」那一格,已由personaStrictAddressing给出)。「从记忆提取画像」提出本条时确实还没有管道、当时列为不做,维护者随后指定要做,已作为本版另一段落地(见上方profileExtractEnabled)。 -
蒸馏的转录被模型当成「该我回话了」:同一窗口确定性失败,重试 20 次、越试越贵(#493) — 转录以「用户:…」开头、裸放在一条 user 消息里(
src/summarize.js的collectMessages),格式指令只在 system 里。细节
模型于是把它当成当前发言接着往下说/点评,输出里连
[都没有,parseSummaryJsonResult在括号早退处直接判invalid summary JSON;而解析失败不推进游标(语义是「这段还没提炼完,下次接着来」),下一个turn/end重试同文同温——同输入同错,窗口还随新事件继续变大。报告者的 20 条失败 input tokens 8194 → 11118 单调递增、finish_reason = stop,正是这条路径;本机注入恒定返回中文点评的假 stream 复现出 19 次额外调用、落库 0 条、游标不动。改法两条:①围栏 + 角色重申——user 消息现在以「你是第三方提炼员,不是下面这段会话的参与者」开头,转录包进<<<会话转录开始>>>/<<<会话转录结束>>>;围栏是确定性的,重试收到的转录与首次逐字节相同(既有回归用例依赖这条);system 保持原样,因为模型对「这段文本是什么」的判断由它所在的 user 消息决定,只写进 system 救不回来(实测同段转录只加 system 约束仍 0/2 出 JSON)。②失败护栏(新键summarizeParseFailureMaxRetries,默认0= 关):连续失败到阈值后强制消费游标跳过该窗口,避免一条会话被同一窗口卡死。默认关是因为放弃内容本身有代价——短窗口偶发失败仍值得重试。 -
解析失败行的审计
metadata恒为null,事后无法回答「模型到底吐了什么形状」 — 成功路径才给metadata赋值,失败早退分支从不赋值——90 条历史invalid summary JSON一条原始输出都没留。细节
现在失败行恒带
distillChars/parse_failure/raw_len/raw_head(截断 300 字符)/raw_has_bracket/streak。raw_has_bracket是区分「模型没按格式写」与「格式对但内容救不回来」的唯一低成本判据,也正是 #493 现场的关键特征。只加计量、不加跳过决策(同 #239 的distillChars取舍)。刻意不改的两处:llm_audit_logs.session_key仍为 NULL(test/pre-inject-gate.test.js:155显式锁着「LLM 调用行不写会话键」,补上会改动 #254 写入准入的话题回填行为);不显式传temperature(温度由宿主/路由决定,插件覆盖会改变所有用户的输出分布)。
v0.9.3
🆕 新增
-
沉淀链接线:把「会自动沉淀工作流」从一句话变成一条能跑的路(
actionNudgeEnabled,beta,默认关) —src/pipeline-nudge.js从 v0.9.1 起就完整实现了「多帧 zstd → 重复序列 → 缺席对照 → draft 草稿」,可它在运行时没有任何调用点——全仓引用只出现在test/pipeline-nudge.test.js里…细节
也就是说机器挖得出来、却永远不会被叫去挖。缺的从来不是算法,是归属与触发:
nudgeFromSessions要的是一个项目的会话桶目录,而桶名是路径的有损变形(规则不在本仓),唯一可信来源是会话头里的cwd。这一版把三处接上:①resolveProjectSessionDir用cwd反查桶,查不到就整跑跳过(skipped:true)而不是降级成全局普查——后者会把 A 项目的动作算到 B 头上,而结果看起来完全正常;②新增nudgeForProject把「归属 → 挖掘 → 落盘」收成一个入口,调用方没有机会漏掉归属解析;③新增src/pipeline-nudge-auto.js挂在turn/end上按项目跑,冷却 10 分钟(观测新鲜度参数,不是性能参数)、单次预算 4 秒、延到setTimeout(0)的宏任务里跑(解压会话日志是同步 I/O,压在回合收尾那一帧上等于让每轮都卡一下)、一次只跑一个项目、抛错只warn不冒泡(session/event那条链上还挂着摘要与续期)。它只产提议,不产草稿:自动跑恒materialize:false,落进<memoryDir>/proposals/<项目键>/;升格仍然要人批,boundaries里那句autoActivate:false与跳过分支共用同一个常量(NUDGE_BOUNDARIES)——两条出口说两句不同的话,读的人就会以为其中一条忘了做。旗标与看图(actionPipelineEnabled)、记账(actionRunEnabled)三件互相独立,开任何一个都不会顺带打开另一个;它进LIGHT_MODE_OFF,是三个 M.O. 键里最重的一个。
🎨 面板
-
M.O. 面板多了「现在沉淀一次」:自动沉淀只在
turn/end之后跑、还有冷却,人想立刻看结果时原本没有入口 — 按钮走POST /api/dsh-mneme/nudge,与自动跑同一条nudgeForProject,区别只有materialize恒关——它写提议、不改仓库。细节
九句回执全部中英双语齐全,且刻意把三种结果说成三句不同的话:
saved(提出 n 条)、none(这一遍没挖到新的重复动作)、unreadable(会话日志读不出来,是环境问题)。后两者在界面上长得一样时,人就只能瞎猜是「没东西」还是「坏了」。会话里读不到cwd时按钮禁用(服务端只会回 400,点了必然失败的按钮比没有按钮更糟);禁用态用虚线边而不是降透明度——整块降opacity会把文字一起压到 AA 以下。顺带把lib/client.js里最后三处「手法」改词,并把actionPipelineEnabled的 hint 从「沉淀和触发都还没做,建议暂时保持关闭」改写成三件能力的真实分工。
📚 文档
-
docs/WORKFLOWS.md新增 §10「自动沉淀:工作流从哪长出来」 — §1 里「一条新工作流从哪来」此前一直是零代码,这一节补上来源——产物表(提议/草稿/定稿各住哪、谁批准)、归属为什么不能拿全机会话去挖、四种reason里unreadable必须与no-bucket分开(前者是故障、后者是关于这个项目的答案,混起来等于把故障报成空结果,端点层因此一个回 503、一个回 200)、触发控制表(turn/end/ 10 分钟 / 4 秒 /setTimeout(0)/ 一次一个,每行都写了为什么是这个值)…细节
以及「提议落盘为什么必须单独一个开关」:
nudgeFromSessions原本只要算得出项目键就无条件写提议,而面板每刷一次就 GET 一次——那是每刷一次往记忆库里塞一批文件。persistProposals因此独立出来,预览路由显式传false,这也是/nudge的 policy 是token-write而不是open的原因。SECURITY.md英中两张表同步登记(/nudge同时进「开放读」与「受门控」两张清单),docs/CONFIGURATION.md补actionNudgeEnabled键与轻量档清单。
🐛 修复
-
术语漂移:整个 M.O. 的「手法」改回「工作流」 — M.O.(Modus Operandi)从 v0.9.0 起在正文里一直写作「手法」,而它与同一个功能在其余位置的叫法(
工作流、workflow、pipelines)并不是同一个词——一个功能两个名字,读者得先猜它们是不是一回事。细节
改法覆盖
src/24 处、面板 3 处、文档 11 处,已发版的 CHANGELOG 条目与测试里作为夹具主题词的「手法」不改(历史不追溯,夹具词锁的是路线图插入逻辑、不是产品术语)。git grep 手法 -- src现为零命中。 -
作用域反查自 v0.8.0 起每次都抛错走降级,canonical 归一与成员校验静默失效(#487) —
scope.js曾裸读ctx?.workspaceRegistry,而workspaceRegistry不在inject声明里(index.js:45)。细节
cordis 4 的
ctx是 Proxy,裸读未注入属性会抛cannot get property ... without inject,而不是返回undefined——于是 registry 反查自 v0.8.0 起必然抛错走降级:canonical path 归一与成员资格校验静默失效,workspace_scope只剩会话原始 cwd。改为与webServer同一形状的免 inject 读取(api.js:205/index.js:583):有ctx.reflect.get时走它,旧宿主的普通对象才裸读;不加进inject——index.js:39-44记着 v0.7.23 移出inject想支持 headless,结果 apply 阶段宿主未就绪直接崩。服务缺失(headless / 旧宿主 / 未激活)是合法状态,静默按「未提供」处理、不告警;真故障(list()抛错)仍warnOnce一次并降级header.cwd。回归锁用「读未声明属性即抛」的 Proxy 复刻真实宿主形状——既有三处 registry 测试都用普通对象伪造 ctx,而普通对象裸读不抛错,所以这条路径此前在测试里看着是通的。 -
/nudge的三处「没看全」被报成「确实没有」(#489 评审) — ① 写盘口——方法分流只拦PUT/POST,其余直落run(null),而persistProposals按「不是GET/HEAD就算写」算…细节
于是带 token 的
PATCH/DELETE会走进写入分支、把提议落进memoryDir:一个不需要 body 的静默写盘口,与这个端点「GET只预览、写动作一律要POST」的卖点正好相反;改为入口白名单(GET, HEAD, POST, PUT),其余 405 +Allow头,在readBody之前拦,测试同时锁状态码、Allow头与「memoryDir下一个文件都不许出现」(只看状态码会漏掉「先写再报错」)。② 预算外路径——归属解析最多同步扫 500 个桶 × 3 个文件,原先完全不看budgetMs,于是「真卡就调budgetMs」这句话对这一段是空话;现在桶循环与文件循环里各查一次deadline,超时回新reason: budget-exceeded,在/nudge上按环境问题报 503(扫描没跑完不是「这个项目很干净」的答案),nudgeForProject自己按同一条截止线转交,避免「解析用 A 截止、挖掘用 B 截止」的双预算。③ 截断谎报——收集桶名时边收边截:数到maxBuckets就break、之后才sort(),于是留下哪 500 个由文件系统遍历顺序决定;目标项目的桶一旦落在被截掉的那部分,函数返回no-bucket,而no-bucket的语义是「扫完了,确实没有」,一次截断就此被报成「这个项目没跑过会话」;改为先收全、排序、最后截断,新增reason: bucket-limit与bucketsTotal/bucketsTruncated两个读数。另修正config.js里「30 分钟/项目」的注释(实际NUDGE_AUTO_MIN_INTERVAL_MS是 10 分钟)。一处如实说明:把sort()去掉(先截断)这个变异没有被测试咬住——测试用临时目录的readdirSync顺序恰好是字母序,两种写法结果相同;要真正覆盖得有能控制readdir顺序的注入点,记为已知覆盖缺口,不假装它被锁住了。
内部与测试
-
多帧 zstd 的反证断言绑了 Node 的整块解语义,legacy 腿下一次 runner 升级就红(#490) —
session-usage.test.js原反证断言zstdDecompressSync(buf)只能拿到第一帧,依赖的是「整块解只吐第一帧」这一 Node 旧行为;24.22.0 起解码跨 chunk 边界也能解出所有帧(nodejs/node#65865)…细节
断言在
test-host-legacy(装 latest 24.x)上过期——同一分支上 24.22.0 两条 legacy 腿全红、main 上 24.21.0 全绿,也就是说 main 那次「全绿」并不证明这个版本问题已经消失,runner 下次升到 24.22.0 就会红。改成数 zstd magic 帧头,既与产品代码forEachFrame的切帧判据同源、又不绑定解码器语义;第二行断言用stat.frames + stat.failed——「帧头数 = 成功数 + 失败数」是实现契约,而「帧头数 = 成功数」只在failed恒为 0 时成立,是巧合不是规矩。已知局限写进注释:这条锁的是「切帧判据仍是 magic」,不是碰撞免疫——payload 里撞上 magic 时计数器与解码器会一起错(半片也解得出来),实测frames同样变成 4,这条测不出来。
v0.9.2
🆕 新增
-
功能开关的第三态「待重启」:把「面板显示已改 ≠ 运行时已改」这个落差摆到界面上(#481) — 功能开关的生效节奏一直是启动期的——
lib/index.js在装配时把 kv 里的覆盖合并进一份 cfg,注入器与后台任务闭包持有那份快照,此后运行中的实例不再重读(index.js装配处的注释早就写明「合并结果只作用于本次启动」)。细节
这是有意为之的设计,不是 bug;缺的是提示:面板上「已改」徽标亮起只说明值进了库,不说明运行中用上了它。实测代价很具体——人格总闸在面板上显示已开启、人格段却整整一轮没注入,排查到最后才发现唯一变量是那份冻结 cfg 里
personaEnabled仍是false,把插件关开一次就好了;而在此之前,用户能拿到的唯一反馈是「怎么没用」。修法分两层,后端只做比较、前端只做显示:featureSnapshot()新增pendingRestart,列出「kv 里有显式覆盖、且该覆盖与冻结值不同」的键(嵌套点号键走既有NESTED_FLAG_PATHS,比较用Object.is而不是!==——NaN 与 ±0 上两者结论相反,开关值里不该出现它们,但「值是否相同」的语义要对齐);面板把flagTag从两态扩成三态(待重启 / 已改 / 默认)并在卡片顶部加一条提示行,写明「重启 DSH,或在插件管理里把本插件关开一次」。为什么必须由后端算:前端手上只有overrides,它能回答「改没改」,回答不了「用没用上」——那需要另一份冻结值,前端拿不到。effective也反推不出待重启,它按「overrides 优先」取值,恰好在有覆盖时把落差盖住了,所以只能是新字段而不是新语义。config缺席时(旧调用方直连、未传 cfg)返回空数组而不编造,pendingRestart恒为数组、恒不为undefined(面板按长度渲染,字段缺失会渲染出undefined.length)。既有overrides/effective语义一字未动,回归锁四条:值不同→列出、值相同→不列、无覆盖→不列、无 cfg→空数组。 -
作用域级可逆闸门:让一路记忆能被单独按下与松开,而不必改配置重启(#471,默认关) — 此前的开关全是进程级——想停掉某一路(比如「别再把工作区 A 的记忆注进来」)只有两条路,改全局配置(一起停,误伤别的路)或逐条
memory_forget(太细,几百条要手工捞,还会漏掉中途新写的行)。细节
缺的不是开关数量,而是另一根轴:本仓早就有完整的事实轴(
memories.agent_scope/workspace_scope与scope_changes审计),却没有任何可逆政策轴——一个「人给的、可撤回的、存续到下次开工的」暂停状态,它与标注正交:按下不改任何一条记忆的标注,松开也不需要把标注改回来。实现是scope_state表(dimension×scope_value唯一索引 +state二值active/paused+note),配src/scope-state.js承载判定,闸门挂在gateByScope旁并前置于既有strictScope硬过滤——于是两条轴串联而不是互相替代:先按人给的政策把整路按下,再按标注做可见性过滤。重复按下同一状态不刷新decided_at(用ON CONFLICT ... DO UPDATE SET decided_at = CASE WHEN scope_state.state = excluded.state THEN scope_state.decided_at ELSE excluded.decided_at END一条语句表达):「什么时候真的变了」比「最后一次谁点了按钮」更接近审阅时想知道的事实,反复点同一个按钮不该把它变成刚发生的事。覆盖范围刻意与既有的四处gateByScope/硬过滤落点对齐(三条检索路 + 注入候选池),并单独接了巩固与睡眠两侧的全部读入口(日间:活跃库尺寸、叙述条 inputs、巩固runDream、总览summaryAll;夜间:冲突消解、归档降级、模式发现、关系补全四个相位)——只改检索与注入会让整理与叙述仍读到被按下的行,那等于闸门只关了一半。睡眠侧尤其要紧,因为那几个相位不只读:phaseDemotion会setArchived与demoteToSummary(真改归档面),phasePatterns会铸出type=pattern的新行——闸门开着而每 6 小时一次的睡眠照样在被按下的作用域上动手,「按下」就只等于让它在白天看不见。两处位置纪律也写进了注释:降级的过滤放在循环之外而不是循环体里continue(一旦将来有人在循环体里加副作用,漏掉那条continue就是静默改到被按下的行),模式发现的过滤排在.slice(0, limit)之前(否则被按下的行照样占掉名额,它们既不会被扫、也不该挤掉别人)。总览那一处是唯一「必须在源头挡」的:总览落库走saveWithDedupe且不带agent_scope/workspace_scope,它是无作用域的 summary 行,而gateInjection只拦匹配暂停作用域的行——没有标注的行永远不会被它拦下。于是被按下的内容一旦进了总览就变成「洗干净的全局行」,此后一直注入;别的入口漏一次只漏那一条,这里漏了是每轮洗一遍。回归用例把这条写成了可执行断言:总览落库后其workspace_scope恒为null,将来若有人把总览改成带作用域的,它会红。两级语义沿用 #254:enabled开判定、enforce开拦截,enforce关时只留审计行而注入照常(线上回滚不必改配置重启)。set_scope_state是唯一新工具,回执如实回显库里当前的decided_at而不是回显入参——重复按下时这两个值不同,回显入参会让界面显示一个从未落盘的时间。默认全关时输出与改动前逐字节一致,且有回归用例锁住这条。
🐛 修复
-
--flag的值以--开头时被当成新旗标吞掉:一次 CI 红背后的真实 CLI 缺陷 — 崩在test (windows-latest, 22)的那条用例是reclaim 默认 dry-run 报数字,报错却是「未配置访问 token」——命令自身没问题,问题在 token 的形状。细节
token 由
randomBytes(24).toString("base64url")生成,而 base64url 的字母表含-,于是约 1/4096 的 token 前两位都是-,拼进命令行就成了--UYoOHxr6…;旧parseArgv只要看到下一个词以--开头就当上一个旗标没有值(token = true),flagValue再按「true等于没给」回退DSH_MNEME_TOKEN与~/.dsh-mneme/cli.json,两者都空 → 报未配置 token。这是随机性,不是测试写法:同一个缺陷平时一律绿,只在 token 恰好掷中这 1/4096 时才现形,因此它潜伏了整整一个发布周期而没有任何检查会红。影响面也不止 token:--type/--title/--content只要值以--开头就同样坏,而这三个值完全由用户输入。修法是把「像不像旗标」从字符串前缀改成登记表:新增KNOWN_FLAGS收录全部取值旗标与布尔旗标,flagKeyOf()认--foo/--foo=bar两种写法取键名,只有已登记的词才算旗标,其余一律当上一个旗标的值——--limit --json这类旧语义(limit无值、json为布尔)分毫不动。选登记表而不是加引号规则:引号要经过 shell 才生效、PowerShell 与 cmd 的转义还不一样,而登记表在进程内就能判定。回归锁六条,最关键的两条覆盖 CI 现场(断言flags的键恰好是["token","url"],token 尾巴不得成为旗标)与「未知--词按值处理」;另有一条文本锁扫cli.mjs里所有flagValue/flagBool调用的字面量键名,要求全部已登记——漏登记的后果正是这里修的静默吞值,所以让它变成一条会红的测试而不是等下一个 1/4096。顺带补invokedDirectly守卫(realpathSync双侧比较,因全局安装走.bin符号链接,字符串相等会误判),使cli.mjs可被测试 import 而不触发main();在旧实现上撤掉修复,这两条会变红,恢复后 6/6 绿。 -
dreamMaxTokens默认 131072 撞服务商硬上限,巩固 180 次失败 0 次成功(#478) —dreamMaxTokens的默认值被原样当作max_tokens发出去,而很多 OpenAI 兼容网关的上限小得多(实测报错是max_tokens 不能超过 65536)。细节
请求在服务商侧就被 400 拒掉,连推理都没开始——
dream_runs在 2026-10-10 ~ 10-18 八天里dream_consolidate/dream_summarize/dream_narrative三个相位合计 180 次失败、0 次成功,而用sleepMaxTokens8192 的睡眠相位和硬编码 4096 的实体抽取一直是好的,分界线就是「预算有没有超过 65536」。两处都修:面板能改了(dreamMaxTokens/sleepMaxTokens两个键其实早在src/settings.js的白名单里,是前端FEATURE_NUMBERS只列了一项,导致 UI 根本没渲染——这类「后端支持、前端没接」的缺口不会报错,只会让用户以为改不了),并且能自己降下来:withMaxTokensFallback照withEffortFallback的同构写法,只在 400 且错误信息里认得出显式上限时降级重试一次(不能超过|不得超过|至多|at most|maximum value is|<=后跟数字才认,认不出就返回null不猜),AbortError直通、cap >= 当前值时终止不循环。降级不是猜数字:把服务商自己声明的上限当依据,比我们闭着眼睛写一个「安全值」诚实。接线锁是本次的重点——dream.js三个使用点和sleep.js两个相位必须全部走withMaxTokensFallback,任何一处退回maxTokens: config.xxxMaxTokens直发都会让面板承诺的「超上限自动降级」变成假话,所以测试直接读源码断言「没有这个模式」并数接入点个数(3 + 2);变异验证:把睡眠侧改回直发即红,还原 8/8 绿。 -
人格卡删除 × 永远不可见(v2 修法) — 卡标签的 × 复用了规则删除按钮的
mneme-set-del样式,其opacity:0只在.mneme-set-row:hover下解除,而卡标签不在该悬停容器里,导致 × 永远透明——功能在、入口不可见(v1.2 实测踩中)。
📚 文档
-
README 不再把「离线」写成默认行为 — 两份 README 合计 12 处「默认全离线 / offline by default / 默认零网络依赖」与代码默认值相反——
autoDream与autoSummarize默认开(src/config.js)…细节
两者都把记忆正文交给
ctx.llm.stream(...)(src/dream.js/src/summarize.js),embedProvider默认仍是openai,只有显式改成local(ONNX)才真正离线。改法:定位句改成「记忆只落在本机」,语义检索改成「把embedProvider设为local后可以完全离线」,隐私节补一条出站事实(巩固与摘要走宿主模型)与一条「想连巩固也离线就换本地模型或关掉两个开关」;dsh-mneme/README.md的「完全离线的语义记忆引擎」改成「可以完全离线运行的语义记忆引擎」。新增test/readme-claims.test.js两条守卫:绝对离线声明不许回来、隐私节必须写出「巩固走宿主模型」——删掉假话却什么都不说,误导是一样的。GitHub About 同步换成不声称离线、补上四路召回与 MCP/CLI 的版本。 -
评审揪出的两处「说了但没说全」(PR #476) — 第一,隐私节的离线条件只写了「把宿主换成本地模型」——但
dreamProvider/dreamModel与summarizeProvider/summarizeModel是配置优先于宿主默认模型的显式覆盖(src/config.js与src/dream.js的resolveRoute,Issue #25 的 config-first 排序)…细节
配了这两组就直连那家 provider,换宿主根本不生效;现已把两组键名一并点名,中英各一处。第二,守卫测试本身在 packed 包里会崩:
files清单把整个test/发出去,而包里没有仓库根README.md,readFileSync在加载阶段就 ENOENT,把「这不是仓库」报成「断言失败」。同一毛病既有的security-doc/release-docs/workflow-guards三条测试都有,本 PR 只是让它多一个入口。修法不改files(那会动发布内容、且只修一条留着三条),改为根 README 不存在时整组skip并写明原因——实测重新npm pack解包后 2 条 skipped / 0 fail。 -
自动蒸馏写入的行 100% 拿不到向量(PR #477) —
scheduleEmbed首行if (txDepth > 0) return;把事务内写入的嵌入直接丢弃,旁边注释写的是「deferred to the transaction's commit」…细节
可
transaction()的 COMMIT 分支从来没有补跑——注释承诺的行为从未存在。而自动蒸馏的每一笔写入都走service.transaction(),所以session:*这些行一条都没拿到向量,「语义检索」对它们是一直静默失效的。这是 #372 的同模板 bug:实体抽取当年查出来入队补跑了,嵌入漏了。修法照 #372 先例——事务内改为入队(pendingEmbed),transaction()提交且txDepth归零后drainPendingEmbeds()补跑,回滚时按embedBaseline截断回本层基线(绝不给已回滚的行嵌向量);队列不设上限,因为cap丢最旧等于把「静默丢弃」换个量级带回来。顺带修searchVector:维度不匹配的旧向量(换过嵌入模型)以前会喂进cosine拿假 0 分混进结果尾部,现在按维度跳过。回归锁 5 条(test/distill-tx-embed-gap.test.js),在未修复代码上 4/5 变红。
v0.9.1
🆕 新增
-
M.O. 从「看得见」走到「跑得出账」:运行账落在项目里,执行窗口由人显式打开(beta) — v0.9.0 的 M.O. 只有一张图和候选,没有任何一条「跑过」的证据——读侧早就能把执行情况算成四态,可账本一直没人写,
runs恒为空数组、所有节点恒为candidate,跳过与绕开的退休提议永远发不出来。细节
这一版补上写侧:
src/pipeline-runs.js把一次 run 落到<项目>/.mneme/pipelines/<id>.runs.jsonl,与手写的.json分开,一次 run 不会冲掉人的 diff。开窗必须由人的显式动作划:工作流的trigger是人话,拿它去匹配用户消息只能猜,而猜错的代价不是少记一笔,是把一条从没跑过的流程记成跑过、还把节点染成可信——所以窗口边界是/mo-run start <id>与/mo-run stop,窗口之外发生的一切一个字都不记,漏记可接受、编造不可。四个取舍都有代价意识:窗口按会话开、一个会话同时只允许一个(再开会被拒而不是顶掉,顶掉会让前一次执行悄无声息地消失);事件先缓冲到 stop 才配对(工具的结果可能比调用晚很久,边到边配会把「还没等到结果」误判成「没有这个调用」);只在显式 stop 结账、不按回合关(一条流程常跨多个回合,按回合关会切成几条半截账);热路径永不抛(挂的是宿主会话事件通道,可选插件的可选能力把 agent 一步带崩,比丢一次抢救严重得多)。「账本坏了」与「从没跑过」严格分开:逐行收,好行照收、坏行进problems(带行号、标出是否只是一截尾巴),状态降partial/broken,并撤掉坏账本的 500——它会让客户端当成离线,还吞掉读得出来的那张图。账本目录自带.gitignore也是为这件事:.runs.jsonl一旦被提交,两条并行车道先后追加同一份账本就会产生冲突标记,而那些标记不是合法 JSON,一份流水污染会升级成工作流状态永久消失。独立旗标actionRunEnabled默认关且进LIGHT_MODE_OFF——看图与写账是两件事,混成一个键会让只想看图的人不知不觉获得写入能力。 -
会话事件翻成观察:只有跑在命令执行器里的调用才算工作流的一步 — 接线前的实测就把原方案否掉了——400 个会话里工具名是
pwsh(1399)/read(1709)/grep(1040)/glob(183),而工作流里人写的tool是npm test、git push <tag>这类命令行,两者不重叠…细节
直接拿 name 去比一条都对不上;唯一能对上的是「命令跑在哪个工具里」,
pwsh的arguments.command才是那条命令行。三条纪律都是「不许编造」的具体形态:非命令执行器一律不产观察(read/write/glob/grep是 agent 的日常动作,硬把文件路径拼成「命令行」去匹配只会制造噪声,而噪声的代价是把某一步凭空染色);只有拿到结果的调用才产观察(配对靠callId,「调了但没结果」既不是成功也不是失败,拿它当一次成功使用是编造,如实进dangling);成败是三态(拿不到就留null,把「不知道」折叠成「失败」会给这一步打上不该有的refuted标记)。 -
归属规则只出候选,绝不下结论 —
tool字段是人手写的自由文本——单命令、多命令并列、带占位符,还有 4 步是空的,拿这种文本做精确匹配注定命中率不高,而误匹配的代价远大于漏匹配(把「推 tag」记成「推分支」,deriveState就会把错误的那一步染成可信)。细节
所以这一层只产「候选 + 理由 + 三种匹配强度(
exact/weak/ 认不出来)」,认不出来是合法答案。弱匹配用词前缀而不是「第一个词相同」:真实工作流里git出现 5 次,按首词匹配会让一次git push报出六条候选,噪声盖过信号;压到 2–3 条之后人才愿意看。报告分三桶(有候选 / 认不出来 / 拼不出命令行),并把结构性无法归属的步骤单列出来——那是「为什么这一步永远没账」的答案。ponytail 复核随即砍掉四块替不存在的写者预留的代码(整块的attributionReport、at/actor两个没人读的字段、SEPARATOR_RE的三个从未出现的分支、PLACEHOLDER_RE的两种从未出现的形态),模块 189 → 165 行。 -
P1/P2/P3 三级落地:从「回执列出干了什么」到「人确认才写账」再到「观察旁挂与脱节检出」 — 三条实测把原方案一步步逼掉——单条命令匹配手写步骤表,唯一归属仅 14.2%(非发版会话 4.4%,区分力只有 3 倍);整段命令当分组键每会话中位 539 组、最多 7373 组,人扫不过来;换动词当键,密度最高的是 PowerShell 管道语法…
细节
说明**「命令 ≈ 动作」这个预设本身是错的**;只留业务命令(git/gh/npm/node/docker)后业务段占 6.5%,那才是人能扫的量级。于是 P1 只报「本次观察到哪些业务动作、各几次」由人对账(
stop在没归属到步骤时不再只说「没有落盘」,而是同时给出观察清单——清单才是这一版真正的交付物);P2 把写账从stop挪到/mo-run confirm——「没确认」≠「没发生」(人没提到的步骤什么都不写,若把「忘了勾」记成「被跳过」,RETIRE_AFTER_SKIPS就会据此发出错误的删步提议),unsure与notDone必须分开,「全部没做」也是一条合法且完整的记录;P3 把两样都有的东西对一遍——人说这步做了 + 声明的 tool 本次一次都没出现(实测背书:gh pr merge --rebase在 21723 次调用里出现 0 次),系统只把矛盾摆出来让人判,不改步骤表。观察走旁挂<memoryDir>/observations/<项目键>/<id>.jsonl而不是给账本加字段——账本是 v0.9.0 发出去的数据契约,为 P3 加字段是动已发布契约。左右各一道隐私闸:观察行只存{key, count, flags?},形状闸验原始输入再构造,任何带command/args/segments的输入大声拒(静默剥离会把上游的形状 bug 藏进一次成功写入);P1 那边同样只保留动词/子命令/旗标、位置参数一律丢弃——回执会显示在界面上、可能被截图,而完整命令行里几乎必然含绝对路径。端到端实测当场抓出四个真 bug(脚本路径被当子命令、git -C <path> log把值当子命令、带点的文件名通过校验、旗标被重复渲染),全部属于同一条红线。 -
步骤新增
assert与optional:机器能不能验完这一步,从此在数据里说得出口 —assert不是普通字段,它是「可断言节点 vs 判断节点」这整个划分的唯一载体——没有它,「哪些步骤能被机器自动盯着」在代码里就没有地方表达(评测报告的实测是 0/19 步,一条流程全靠人判,而那个事实当时没处可写)。细节
加它要一次改四处(校验 / 归一化 / 节点投影 / 首屏计数),漏任何一处症状都是「加了但面板看不见」且不报错,所以先装一道字段白名单锁再改:往步骤里塞
assert/onFailure/retries,三个全部静默消失,正是这类「声称改了但产物里查不到」的同型装置。两个语义决定:没写断言时投影成null而不是空数组(后者会让「作者没写」与「写了但一条都没通过」长得一样,而验收时这是两件事);首屏新增assertable计数与既有contrast并列(后者数「写了没有」、内容仍靠人判,前者数「机器能不能判」,0/19 是实话不是缺陷,但清单上必须看得见)。optional缺省false(=必经),显式落键而非缺省缺键——「这步可不可跳」是判据要读的语义,读侧拿到的一律是布尔;存量步骤表零改动。 -
R1/R2:跳过有了性质,前置违规与 tool 过时都有了提议 — 同一句「这一步总被跳」,在可选步骤上是不该提议删的(只发
optional-skip-ok信息项),在必经步骤上则同时意味着「删掉这一步」与「这条路不对」两种读法,两种提议并存、谁也不吞谁——第一版把drop-step挂在else if里永远到不了…细节
被自己的测试抓出来。R1 的前置违规跨整本账本逐 run 查(保住 run 边界,
tallyRuns已经把 run 抹掉了),按run|step|need去重、同一步对不同前置各报一条(修图的人要知道断在哪条边上),一次就报。retirementProposals的封闭问题表从两类穷举到五类——新类不配问法会落进「岔路」那条毫不相关的问法里。 -
沉淀链:从真实会话日志里把反复出现的手法挖成 draft 工作流,但判据不给结论 —
src/pipeline-nudge.js走「多帧 zstd 逐帧解 → 重复序列候选 → 能算的缺席对照 + 答不出的封闭问题 →status:"draft"草稿(过validatePipeline)」。细节
buildCounterfactual().verdict恒为null:机器只写观察到的,其余留「(待补)」,答案只能由人回答、经applyAnswers写进steps[].evidence[]。专家团复核抓出七处,最贵的一条是「空回答不算已答」——applyAnswers(c, {})与applyAnswers(c, null)曾返回ok:true并推进到answered,那是判据链路上唯一一处「没人回答也算答了」;另有一条把「机器不给结论」从产者的自觉变成边界的性质:校验层拒绝任何非null的counterfactual.verdict(缺键也拒)。其余是跨工作区不并账、unanswered补回出口、草稿与提议改原子落盘(一个半截文件曾能让整个项目层的工作流从面板上消失)、按层过滤后的相邻不许当真相邻、failures不再用null同时表示「未接线」与「确实没有失败记录」。 -
人格定义页 v1 → v1.2:从「一张卡」到「按 agent 寻址的多张卡」(beta,默认关) — v1 是存储/API/开关/注入/面板五件套(
user_settings加persona键、PERSONA_FIELDS白名单挑拣、坏 JSON 容错回空对象、GET+PUT /persona双面路由、inject.js里人格段前置而rules恒最硬、任一闸关时输出逐字节等于历史版本)。v1.2 按实测三反馈升级:设置键变成 `{default…细节
byAgent}
多卡形,**v1 的单对象读作default、零迁移**;注入按会话身份(session.header.agentPreset,与scope.js同源读法)寻址对应 agent 的卡,身份缺失/无卡/卡关一律落default;面板改多卡列表,补三个字段的可见标签(v1.1 反馈:aria-label看不见)与保存失败可见错误行(v1 的静默catch吞掉了 404,实测踩中)。**一次修好又回滚的教训**:卡标签删除的×复用了规则卡的mneme-set-del样式,而那个类只在.mneme-set-row:hover下解除opacity:0,卡标签不在该悬停容器里——功能在、UI 永不可达;修复(内联opacity:1`)后又因同一原因整体回滚(#459),留作已知问题。占位示例也从使用者自己的两个人设名换成了中性名。
🐛 修复
-
面板的检查更新把「成功」报成了「失败」,星星入口指向了「谁已经点过星」 — (#460):面板拿
updateInfo.status去比一组后端classify()根本不会返回的名字,于是成功的检查一律落进错误分支。细节
顺手补一道跨文件契约锁双向钉住(面板比较的每个状态必须是
classify能返回的值,取数路径必须对得上注册的路由),并把「重新注入错名字」作为判别验证——新锁会点名它。星星入口原先链到/stargazers,那是「已经点过星的人」列表、页面上没有 Star 按钮,改为仓库主页。 -
向量的「有向量」不再是一句空话:写入回执、行级模型出处与模型感知的回填选择器 — (#450):
embedFor/schedule返回结构化回执(ok/failed/skipped+ 原因)且schedule不再吞掉 promise,失败进 200 条环形账本(不接llm_audit_logs:高频回填会淹没 LLM 调用的追溯面)。细节
vector_meta.model_hash是「最后一次成功写入的模型」,换模型即被整表覆盖,无法回答「这一条向量是谁产的」——memories新增embedding_model列做行级出处,存量一律NULL(出身未知),不按时间推断模型名(猜出来的名字会被日后审计当作证据,而它不是);清空向量时连同出处一起清。回填与reindexMissing改走新选择器(空向量 OR 出身未知 OR 模型不匹配),换模型后旧向量才真正被重算,而不是永远占着「有向量」名额虚增覆盖率;maxTotal=0的语义定为一灌到底,启动期回填的三个写死数(500/10/200)改成配置项,返回体让capped与drained不再不可区分。 -
账本的 ids 跨块引用被
catch静默吞掉,fix-step-tool永远少发而界面无异常 — (CodeRabbit #458 四条里核实的三条):F1 账本ids原声明在if块内,观察旁挂那块跨块引用必ReferenceError,被外层catch吞成「没有观察」——提升为 handler 级runIds…细节
回归测试走真 handler + 真旁挂文件锁住「观察进了判据」这条正向信号(负向断言抓不到它,因为吞掉之后一切看起来都正常)。F2
config.memoryDir默认值是~/.dsh/memory,而observationDir不展开~,readdir永远落空,补expandHome与candidates.js同构。F3rework-path的两种成因(必经步骤被跳 / 被绕开成常态)共用一句问法,跳过场景答非所问,改为提议带cause字段、nudge按成因选封闭问法、旧形状退回原问法。 -
candidates.js的每一个换行都是「双 CR + LF」,从出生一路进了 main 又发到了 npm — 各 454 处。\r在 JS 里是合法空白,解析、加载、1869 条测试全绿——当时没有任何检查会红。细节
代价落在别处:任何改动这个文件的人 diff 都是整文件 908/908,噪声盖住真改动;同一份文件不同工具数出行数不同(
Get-Content908 行、编辑器 454 行);发布产物里带着畸形行尾。只动行尾、一个字符不碰(字节差 454 恰等于处数,剥掉全部 CR 后与 main 原版逐字节相同),并补一道扫字节序列本身的锁:既有的lib-smoke字节一致性锁抓不到它,那个锁只比 src↔lib 彼此是否一致,两边一起坏就一路放行;新锁直接查这个序列并报出文件与处数(注入 534 处双 CR 后精确变红、还原回绿)。 -
桌面版装的是 0.26.1,落在
dsh-better-sidebar的^0.18.0之外 — peer 是 optional 且消费端全防御式(服务注入 +typeof检查,issue #88 模式),范围只反映生态兼容面而非 API 锁,故放宽上界到<1而不是逐版追加。 -
合并人格页后
effective键计数锁少了一键 — 冲突时 git 把两边的加法串自动拼上,但只体现了一个+1——实际上两边各加一键(actionRunEnabled与personaEnabled),应为 99。细节
顺手把那条 26 项加法串收成一个字面量:那串每合并一次就要重算一遍,拼得越长越没人验算,换成 99 配一行注释反而可查。同步重新生成
lib/(合并时lib/config.js与lib/settings.js的冲突标记是脏的,sync后清零)。
🎨 面板
-
记忆页三栏改两栏、实体页去冗余、热力图改成按窗口分位染色 — (#448,三轮复检):记忆页左栏加宽到 264px 让四个重要性 chip 排成一行,重要性块跟在检索区之后而不是钉在底部;实体页搜索与计数吸顶、按类型分组的侧栏、画布 320 → 440、补「没有匹配」提示;设置页去掉自动弹出的版本横幅…
细节
改手动检查更新按钮(带行内结果)+ 星星入口 + 版本页脚,并把 M.O. 开关提到首屏(首屏预算 8 → 9,注释同步改);实体侧栏补
box-sizing: border-box——width:100%加水平内边距会把 224px 的栏撑破,overflow-x: hidden再把「×N」提及数裁掉。热力图撤掉写死的阈值(750k/1.5M/2.5M)改用每窗口分位分桶,低使用量的库也能铺满四档、空窗口降为 0 档,网格收到 GitHub 贡献图口径(11px 格 / 2px 间距 / 月份标签 / 一三五标注)。顺带补了一道反向 i18n 锁:既有的「每键恰好两行」计数锁看不见「零行」的键(它从计数里消失了),而Icon遇到未注册名字返回null,于是新增「每个字面量t()都得有词条、每个传给Icon的名字都得已注册」的锁(73 → 74),并做过「临时删掉某键两行词典 → 旧锁仍绿、新锁点名」的判别验证。
📚 文档
-
门面加「最近更新」、补四种安装来源、修掉一圈文档漂移 — (#466):根 README 新增 News 表(日期 | 版本 | 头条)与四种安装来源对照(npm 包名 / Git / tarball / 本地绝对路径),并写明
#path:不可省的理由——省掉装到的是仓库根那份壳(0.0.0、无dsh字段),宿主不会把它当插件…细节
github:slow-stack/mneme这种写法实测装出来就是个_pnpmPlaceholder空壳,正确写法是#path:/dsh-mneme。同时补 M.O. 章节、补路线图 v0.9 行、删掉两个 README 里早已不存在的幽灵键sessionLifecycleEnabled(src/全无命中,v0.7.11 已清理)。子 README 的「模型工具(9 个)」改成 10 个(漏了memory_register_document,而同一份文档的架构图里早就写着 10——自相矛盾比写错更容易被忽略)、「9 种记忆类型」改成 10 种(补document指针,issue #230)、设计文档 5 篇 → 11 项。 -
SECURITY.md支持版本表走到 0.9.x,并删掉一个不存在的安全供应商 — (#467):英中两张支持版本表同步到0.9.x,停止维护通知改成「尽力维护」而不是写死某个版本会终止;功能描述里写死的版本号(Standalone External API (v0.7.12, opt-in))全部去掉——功能描述不该绑版本。细节
删掉
OrbisAI Security六处:仓库里没有任何其他痕迹,它是 v0.7.1 一笔提交带进来的幽灵供应商,换成 CI 实况(gitleaks 全历史 + dependency-review 看 PR diff + OSV-Scanner 看 lockfile + Dependabot 告警 + secret scanning)。中文段此前整体落后英文段一个版本,这次一并补齐——包括「未配置 token 时远程端点实际处于禁用状态」这句,它正好是 fail-open 真相的反面。配套新增test/security-doc.test.js六条断言,其中两条把「开放读清单 / 受门控清单」与createApi真实装配出的route.policy双向比对——写进文档的路由清单从此不再可能悄悄漂移。
🔐 安全
v0.9.0
🆕 新增
-
M.O. 给图留一个出口:绕行、候选分层与「跳过 ≠ 绕开」(beta):一个只会被照着走的图会让 agent 失去自主性——它要么沉默地服从(照着走,做错了也不知道错在哪),要么沉默地偏离(觉得不适用就跳了,不记账,没人知道这次执行了几成)。一条规矩如果没留出口,走不通的人不会遵守它,会自己凿一个而且不告诉你。所以图上必须有一扇合法的逃生门:agent 判断「这条路这次可能不对」时,能按风险档决定记账先走还是停下问人。分界是一句话:护栏,不是轨道——允许偏离,但偏离要响。分档(2026-10-16 拍板 B 档):自报 R0/R1 且不跨闸门 且兜底清单没命中 →
decidedBy: "model",记账先走;其余 →"human",停下等点头。一句话:自主性住在 R0/R1,刚性只住在它必须住的地方。 三条各自独立、都要过:① 风险档是自报的,单靠它不够;② 兜底清单(IRREVERSIBLE_PATTERNS)机械匹配、不看自报——自报说 R0 但命令里有git push照样拦,这是「模型在自己上下文里给自己打分不可靠」的直接应用,不是不信任而是不给单点判断(清单可审计可 diff 可追责,让模型再判一次只是把同一个错误问两遍);③ 闸门保护——这条绕行如果走下去、图上某个gate会永远不被经过就拦住,与风险档无关(闸门本来就放在「错了很难撤」的地方,绕开它等于把安全阀绕掉,哪怕全是只读动作)。两条路由读的是两处不同的东西:/pipeline读定稿(住仓库、可提交可 review),/candidates读候选(住<memoryDir>/candidates/<项目键>/、只在本地)。候选不进仓库是刻意的:它是草稿,一天可能长十条死九条,进仓库就是垃圾、也会被当成规程引用。候选是定稿的唯一入口,定稿只放人亲手批过的东西——这正是「一个会自己起草 SOP 的学徒」那句的字面意思。<项目键>取「目录名 + 路径哈希前 8 位」:同一项目每次算到同一个键,但不把绝对路径写进文件名(那里面是本机用户名)。升格两条都要满足:走过两次(至少一次worked——「复用第二次才算类型,第一次只是实例」)且写得出反事实;够格之后系统提议、不自己写进仓库(提交是 R1、推送是 R2,分开两档、人始终握着一道),promoteCandidate()只返回内容不写盘,补出来的东西照样要过validatePipeline——升格不是降低标准,是补齐标准。怎么死:只走过 1 次且 30 天没再被走到 → 归档(dropped,文件留着当负清单的邻居);failed直接进负清单(走不通是知识);人否掉记理由。候选不需要人维护——它只增不减地长,死掉的是「没被再走到」的那些。「跳过」与「绕开」是两个指标,这一版把它们分开了:跳过 = 这次不用这一步(≥3 次 → 提议删掉这一步);绕开 = 这条路整个不对(≥3 次 → 提议重看这条路径)。混在一起数,两种提议就都发不出来。绕行不给节点染色——被绕开不等于这一步坏了(可能只是这次不需要),染色留给真正被推翻过的refuted(唯一有硬判据的信号)。四处只有实机才现形的坑:① 兜底清单第一版把工具名当成动作,于是gh pr view(只看一眼 PR)也被拦下,直接毁掉「自主性住在 R0/R1」——工具名 + 子命令才是动作,条目因此拆成commands/requires/forbids三层;②requires与forbids的语义不能混:requires 表达「必须出现才算数」,forbids 表达「出现就排除」,对「默认安全、显式才危险」的动作(gh api不写方法时是 GET)只能用 requires,用 forbids 表达「默认安全」会让只读调用被误拦;③ 三种处置第一版写成if/else if,绕行把跳过吃掉了——一次绕行很可能同时是「这一步这次没用」+「从这里岔出去了」,两件事驱动的提议不同,吞掉一个就少一条提议,改成各自独立累加;④ 变异验证本身会假绿:node --test --test-name-pattern在零匹配时照样报 pass,拿断言里的提示语当用例名跑,得到的是「变红 ✗」的假象(同类的还有「只查最终树的冲突标记」)。边界(写在契约的boundaries里,免得以后有人以为忘了做):autoPromote: false/autoApprove: false/injectIntoMemory: false——不做自动升格、不做绕行自动批准、不把候选塞进注入路径,也不做「图自动重写」(图的变化一律经过提议 + 人批,回写是新版本)。执行侧仍未开始:所以这一版只有判定与落账的代码,没有触发绕行的地方,提议与批准的动作也还没有落点。回归锁 24 条进新增的test/candidates.test.js(分档、兜底清单的 34 条子命令判定、requires/forbids 语义、项目键不含绝对路径、候选按形状归并、两次才够格、失败留痕、归档不删文件、升格必须过validatePipeline),另 7 条进test/pipeline.test.js(跳过与绕开分开计数、绕行不染色、两种提议互不吞并、suggestions字段必须在、候选路由与三条边界);关键规则逐条变异验红(兜底失效、闸门失效、子命令不分、候选不归并、reason 不再是硬要求、升格跳过校验、归档改成删文件、绕行吃掉跳过——各自精确红对应那一条)。 -
设置面板把折叠线划在「新手之外」而不是「冷门」,条目名改说人话(issue #427,纯 UI 层取舍与文案):issue 实测的账是——「功能」卡 24 个常显开关,分组名(基础/检索与增强/巩固与睡眠/多会话隔离)是架构图的分区而不是人的问题,条目名是术语(
bm25SearchEnabled/preInjectGate.enforce),折叠线划在「不常用」上,于是新用户敢动的只有 3–4 个,其余 20 个要读文档才敢碰。本次只动lib/client.js(手写文件,不参与 sync)与测试,config.js一位未动——验收红线就是 git diff 不出现任何.default(...)变更。①折叠线挪到「新手之外」(建议 1):新增FEATURE_FIRST_SCREEN = ["group.basic", "group.scope"],新首组只装 3 个总开关(autoInject/autoSummarize/autoDream),作用域组整体留在首屏——「它记的东西会不会被别的会话看到」是新用户的必答题,藏进折叠等于没答;其余组整组收进新的「全部设置」第一层折叠(showAll),原「高级」降为它内部的第二层(showAdv保留,7 个条目一位不动,标签「更少用的 7 个开关」与数组长度互相锁)。首屏可点项 = 3 总开关 + autoInject 的 2 个子开关 + scope 2 开关 + 「全部设置」按钮 = 8,压在验收线 ≤ 8 上。成员是从 core/dream 组里移走而不是渲染时过滤——renderGroup是唯一的组渲染路径(g.items.map(flagRow)字面量保留在其内部,inject-parent-gate.test.js的门照过),同一项因此只可能渲染一次。scope 组不拆进 basic 还有个现实理由:client.test.js:991的组成正则锁着key: "group.scope", items: ["scopeEnabled", "strictScope"],整组留首屏让既有锁零改动。②条目名与 hint 重写(建议 2):22 个条目中英成对改写——名 = 一句「打开会怎样」(「顺带带上最近几轮原话」「别人设为私有的记忆我看不见」),hint = 「开:…;关:…(术语,默认 X)」三段式,术语与默认值沉到句尾;writeAdmission与preInjectGate两级在 hint 里互相指路(「要真拦,见下一项/需先打开上面那项」),hybridInject/selectiveInjectEnabled/adaptiveThresholdEnabled按建议 5 点明是「带什么回来」的三档而不是三个不相干的并列开关;「默认 X」逐条对照过src/config.js的 schema 值。删掉basicToggle/advancedToggle.hint三对旧键,新增group.basic与allToggle,features.desc补一句「先看常用,其余收在全部设置」。③「默认 / 已改」徽标(建议 3):FeatureRow加 tag prop,flagTag(k)读state.overrides并用hasOwnProperty判「已改」——settings.getFeatureFlags()无值时返回{},键不在其中就是仍取 config schema 默认值,面板自判、不加后端字段、不动 API 面;两个徽标类照抄 contrast 测试已验过的语义色组合,test/contrast.test.js零豁免通过。回归锁进test/client.test.js:首屏层组成正则、basic 成员必须从旧组移走(负向断言 core/dream 组头不再以这两个开关开头——防「渲染过滤」式回归造成双重渲染)、restGroups只在showAll展开后渲染且「高级」按钮序在其后、FEATURE_ADVANCED_BOOLS恰 7 项、徽标与分组/折叠键中英成对(occurrences ≥ 2)、判定必须走 overrides 而非后端字段。既有锁(inject-parent-gate、A4 scope 组、i18n 计数、contrast)全部零修改通过,全量 1772 tests 全绿。 -
M.O. 从「全局」改成「项目级 + 有名字的清单」(上一刀的回填):上一刀把工作流本体放进了插件包自己的
pipelines/,于是不管在哪个项目、哪个会话打开,读到的都是同一套——「项目级」这个词当时根本没落地。发布前的实测反馈指出:不管在哪个项目,读到的都是同一套工作流。根因三处都在这一刀自己写的代码里:①defaultPipelineDir()取的是import.meta.url的上一级,也就是装了哪个包就读哪个包的;② 面板把它注册成右栏页面类型,而右栏是面板级、与项目无关,会话头那个座位的标签又写死成「M.O. · 手法」,于是到处长一个样;③ 一进面板就是一张图,id与title只画在图顶一行小字,没有能点选的条目——Hermes 那边是一串能干活的条目,——条目列表才是这个功能该有的形状。改法:本体搬到<项目>/.mneme/pipelines/<id>.json,读的时候项目层在前、全局层(包内pipelines/,今天为空且有意为空)在后,顺序即优先级;同 id 两处都有则项目层赢,并把被挡住的那份记进shadowed——不静默丢弃,否则以后有人往全局层加一条同名工作流、发现它「不见了」,会去翻文件系统而不是查这条记录。项目层的坏文件进unreadable(带scope/dir/detail,detail点名是哪个文件)而不拖死全局层:一层写坏不该让人连兜底那份也看不见;全局层是自家发出去的包,坏了照抛。「默认项目级」这句话的落点就在这里,而它不只是口味:本机 7,647 条记忆里带workspace_scope的只有 227 条(≈3%)、活跃的 2,629 条里 2,517 条(95.7%)为空——默认全局等于把一条本地规约推给所有项目,会原样重演「全局变成谁都不看的杂物堆」。project由客户端从会话读出来带过去,服务端不猜:插件侧的ctx上只有logger/llm/reflect/webServer/inject/commands,没有 cwd;取不到时如实标「项目未知」,绝不回退process.cwd()——回退会把「读不到」变成「读到别处」,而后者在界面上看不出来,是最难查的一类错。路由GET /api/dsh-mneme/pipeline增加?project=<绝对路径>,不是绝对路径就回 400project-not-absolute:忽略它比拒绝危险得多——忽略会静默退成「全局」,而全局看起来一切正常。面板第一屏从「一张图」改成清单 + 图同屏(清单当选择器:名字 + 作用域 + 步数/闸门数/对照覆盖率 + 状态计数;画布在下面,默认展开第一条,点别的条目只是换图)——中间那一版做成「清单 ⟷ 图」两屏来回切,结果画布藏在一次点击后面、界面上没有任何提示,反馈连着两次说「看不到画布和节点」;作用域标签只取目录名(projectLabel()),绝对路径里有本机用户名,而面板是会被截图的那一面。两处必须拆成两个组件的地方:清单与图不能在一个组件里按openId分支——hook 不能条件调用,那样写会在点开那一下炸成「Rendered more hooks than during the previous render」,运行期才看得见;投影缓存也按项目 + 按工作流分桶,否则同一个进程里开着两个项目时会把 A 项目的清单端给 B 项目看,而画面看不出错。回归锁:test/pipeline.test.js14 → 18(清单行自带规模与作用域、同 id 项目层赢且报出遮蔽、项目层坏文件不拖死全局层、不给项目根就只读全局层),其中原来那条「pipelines/必须进package.json的files」原地反转成「不许在包里」——两头都防:进包则装完之后每个项目都能读到它,「项目级」又悄悄退回全局级,而这一幕在任何一次本地测试里都看不出来(测试读的是仓库目录);test/client.test.js三条既有锁按新签名改写,并新增「第一屏是清单不是图」。全量npm test1796 / 1795 pass / 0 fail / 1 skipped。同一段[Unreleased]里上一条 M.O. 文末那句「package.json的files漏了pipelines(已补,并加锁)」在本条被反转,两条一起发布时以本条为准。一处只有实机才现形的自伤:新写的详情屏容器与既有的「节点详情」面板撞了同名类.mneme-pipe-detail——后写的flex:1赢下冲突,先写的max-height:180px; overflow:auto却留着,于是详情屏被钉在 180px、里面的画布flex:1分到 0 像素,屏幕上只剩一片空白加一条滚动条(截图确认)。只查颜色的test/contrast.test.js抓不到这类撞车,所以补了一条通用的类名撞车锁(.mneme-pipe-*每个类名只许有一条规则,连扫描量一起钉住,避免正则失配时假绿),并给画布与窄态列表加了高度下限——flex:1的基准是 0,祖先没有确定高度时(浮层与居中 sheet 都是max-height封顶)画布会「在 DOM 里、但看不见」。窄态阈值从 640px 降到 320px:那张图是竖链、固有宽度只有 174px,栏窄一点反而更该画图——640 那档是按当年摊得很开的假数据定的,真数据下它把右栏(通常 400~600px)整个推给「一列节点卡片」那一支,于是「看不到图谱」。缩放条也提到宽窄分支外面,成了「新代码到没到」的肉眼标记。画布可缩放:−/+/ 「适应宽度」三个按钮,外加 Ctrl/⌘ + 滚轮。缩放靠改外层容器的百分比宽度而不是transform——百分比改的是布局尺寸,容器照常出滚动条,平移顺带就有了;transform只改绘制,放大出去的那一半永远滚不到。滚轮那一条自己挂非被动监听:React 的onWheel是挂在 root 上的被动监听,preventDefault()会被浏览器忽略(控制台那句Unable to preventDefault inside passive event listener),Ctrl+滚轮会变成「画布和整页一起缩」——这个错本地测不出来,所以测试锁的就是这两个字面量。一处只进浏览器控制台的自伤(实机复现):右栏那个 tab 的类型注册与正文座位注册原本写在同一个try里。宿主对重复的类型 id 会抛(sidebarRight: tab type id "…" is already registered),而这两件事的生命周期并不一样——类型挂在宿主服务sidebarRightTabs上、插件重挂时留着,正文座位挂插件 scope、随插件回收。于是第二次挂载时类型那一抛把正文一起带走:界面上只剩宿主的 fallback「这类内容还没有可用的查看方式」,而catch里只有一句console.warn,界面上零痕迹。改成两个try各自兜底 + 类型注册前先tabs.get(id)幂等判断;回归锁按「类型注册块里必须有 catch、且必须出现幂等判断」来钉(把幂等判断去掉即变红)。已知边界:画布观感未动(后续再补);执行侧与运行记录写入仍未开始,所以清单上每一条的状态计数都还是「候选 19」。 -
M.O.(Modus Operandi · 手法,beta):服务端旗标 + 客户端三面:新增 feature flag
actionPipelineEnabled,默认 false(opt-in)。config.js的 schema 与settings.js的白名单成对登记,test/api.test.js的旗标计数锁 96→97,并补默认值与 PUT 往返断言——只改白名单不够:effective 的默认值只从config.js的 schema 来,缺 schema 时无 override 会静默少一个键。显示名给全名「M.O.(Modus Operandi · 手法)」,缩写不靠注释解释、靠设置项与 hint 的第一句让用户第一眼看见;内部标识(旗标键与数据契约mneme.pipeline/v1)保持原样,改名纯亏。客户端在lib/client.js,三个座位(会话头 action、对话行卡片、右栏 tab)全部在旗标为真之后才注册,无旗标时一个座位都不注册;模块级 inject 仍只有["slots","locale"],右栏是软依赖。本阶段只到「界面就位」:服务端没有 M.O. 的表与路由,界面接的是内置样例数据,旗标默认关,因此对用户可见行为零影响。 -
会话蒸馏写入也盖作用域章(项目记忆的地基):
scopeEnabled开启时,此前只有memory_save那条工具路径标注agent_scope/workspace_scope,而机器提炼是写入量的绝大多数——实测某次真实部署的库中 7,385 条source='session:*'只有 1 条带 scope,于是「项目记忆」在库里几乎不存在。现在summarize.js复用同一把解析器(scope.js的createScopeResolver)与同一把闸门(scopeEnabled),值取session.header的agentPreset与 registry 反查的 canonical path(header.cwd兜底,取不到落 NULL,解析绝不阻塞写入),来源标'auto'——auto 不构成硬过滤,只吃软加权。合并进既有行时不盖章:作用域属于那一行原来被提炼出来的语境。关闸门时一个 scope 字段都不加,老行为逐字节不变。副作用:去重键含 scope,新盖章行与存量未标注行不再互相合并,过渡期可能出现同题重复——跨项目隔离是本次想要的效果。 -
面板补上蒸馏留存的两个开关,文档里那句「面板可启停」不再是空头支票(#399 跟进的第三步):
docs/CONFIGURATION.md写着distillRecords.enabled与distillRecords.retentionDays「两个键都走 feature_flags 白名单(面板可启停)」,而后端确实全都就位(config.js的 schema、settings.js的白名单、api.js的NESTED_FLAG_PATHS)——唯一缺的是面板:lib/client.js的FEATURE_GROUPS是一份写死的清单,这两个键在该文件里全历史零命中。用户照文档去找开关,够不着;验收这个功能只能绕开产品界面直接写库。现在留存总闸挂在 core 组末位,保留天数收在它的子块里、只在总闸开着时展开(关着一行不写,改天数没有意义),子块按组尾渲染所以总闸必须在组末。新增的整数输入行type刻意用text+inputMode="numeric"而不是number:number 输入框在部分内容被判非法时e.target.value会变成空串,草稿与用户所见脱节,就没法把非整数挡在本地并退回生效值。中英各两条文案,hint 讲清两件事——这是原始对话落盘(隐私面,所以文案要说明私密推理与工具结果不落),以及天数只管启动期裁剪(reclaim 走另一条--older-than,不读这个键)。8 条变异逐条验红(草稿不从 effective 初始化、天数行不看父开关、非整数不退回生效值、enabled 从组里删掉、数值键不进清单、中文文案删掉、子块不接到组尾、数值行不从 numRow 渲染);其中头一条断言起初写成子串匹配,把eff["distillRecords.enabled"]也算了进去——删掉组里那一项照样全绿,改成把 core 组的items数组抠出来单独看才成立。检验「某容器里有没有 X」必须定位到容器再查,子串断言会被别处的合法引用蒙混过关。 -
M.O. 从「界面就位」走到「读真数据」:工作流的本体住仓库,面板不再画假数据:上一版客户端三面接的是内置样例数据(
lib/client.js的PIPELINE_MOCK),服务端一条路由都没有——界面能开、形状对,但图上那 8 个节点与 3 条运行记录全是常量。这一刀补上读侧与数据面,并把...
v0.8.16
🆕 新增
-
面板「Token 去向」三屏:把账本里已有的成本数据摊开(issue #412):原「LLM 消耗」卡只有一个总数与调用次数——
getLlmAuditStats早就返回了by_source/by_status却被丢弃,用户看得见花了多少、看不见花在哪。后端新增两个聚合并原样保留by_source/by_status(既有形状锁不动):by_operation按operation_type分组,因为trigger_source太粗——它把dream_consolidate与dream_summarize并成一个autoDream,恰好把 2026-10 审计里最大的一项(7 天 token 的 37%)藏起来;by_day按天升序给趋势图供数。前端把单数字卡换成三屏:KPI 带(7 天总量 / 调用次数 / 巩固占比 / 待确认冲突,后两项分别来自by_operation与mneme:conflicts-changed事件、与 DreamStatusCards 同源)、按组件 donut(半径 15.9155 使周长≈100,strokeDasharray可直接填百分比;段间留缝;write_admission只计量、token 恒 0,不画弧只留图例)、7 天柱状(纵轴上限取整到整 M)+ 8 周热力(右缘对齐数据末日、左侧补齐到周一,超窗格子隐藏)。「缺日补零」与「未到 vs 已知为空」严格分开:stats为null(请求还没回来)整块不画柱,{}(已知为空)才补七根零柱;8 周热力单独请求days=56,复用 7 天那份数据会让 48 格恒为 0。原 9 张状态卡降级进折叠区,开合开关挂在summary而不是details上——挂在details会把子卡片的点击一起吃掉。面板侧 6 条回归锁进test/client.test.js,三条变异检验(缺列不补零、热力复用 7 天数据、开合挂错节点)各自转红后才还原。 -
蒸馏原文留存 + 记忆级溯源(issue #399,opt-in 默认关):蒸馏是一条有损管道——它把窗口喂给 LLM 拿回几条记忆,然后消费掉游标,原文此后在库内没有任何痕迹。后果是三件事同时成立:记忆没写进去的细节事后无法回查(不是没保存,是没地方找);一条记忆说不清自己出自哪段对话;蒸馏质量出问题时没有基线可比。信息论依据(arXiv 2609.36059 §3 Eq.1)很直白:旁路索引不丢信息——
I(Y;M,H,X)=I(Y;M,H),留存原文不改变已蒸馏产物的互信息,但取回原文的能力被保住了。所以这不是「再存一份」的冗余,而是把单向替换变成可回指的叠加索引。形态是两张只增不改的新表(CREATE TABLE IF NOT EXISTS,无 ALTER、无回填、不动memories):distill_records(蒸过的窗口原文,主键(session_id, seq),INSERT OR IGNORE幂等——同一窗口重蒸不翻倍且保留首次落盘时间)与memory_provenance((memory_id, session_id, seq_start, seq_end)唯一索引 +ON CONFLICT DO NOTHING,每条新记忆回指来源窗口)。写入点在summarize()里顺手落:那里已经持有会话事件、已经算出窗口边界,不新建常驻管道、不新开监听器,于是留存范围与蒸馏范围天然一致;落盘与记忆写入同事务(游标消费也在同一个事务里),不存在「记忆写了但原文没落」的半截状态。最要紧的约束是隐私谓词只有一份:渲染与落盘都从publicTranscriptParts(event)取数,因为两条路各写一遍过滤逻辑必然漂移,而漂移方向必然是「存下来的比 LLM 看过的更多」——模型没看到的私有内容反而进了持久库,最糟的方向。口径:真实用户提示与助手公开text块两条路都进;私有推理链、插件注入的机器内容(AGENTS.md / skill 正文 / 文件变更通知)两条路都不进;工具调用入参与结果、code 输出只进渲染、不落盘——这是刻意的路径差异而非漂移(codingRetrospect靠工具输出沉淀踩坑根因,渲染路径必须带它;但它体积大、含路径与命令、不属于对话内容),差异方向仍满足「落盘的 ⊆ 渲染的」。留存文本不受distillMaxChars(默认 24000,超长保尾部)与工具裁剪trim(args,300)/trim(out,500)约束——截断过的「原文」没有回查价值。删除语义两分:到期裁剪只删distill_records,溯源链接保留指向空区间(面板显示「来源已过期」而不是查无此记忆),绝不级联删记忆;删记忆是另一条路,store.remove(id)在事务内显式清链接(不靠ON DELETE CASCADE——store.js从不启用PRAGMA foreign_keys,翻它会顺带激活recall_events → recall_runs那条既有约束的行为,不是本次范围)。配置照llmAudit形状:distillRecords: { enabled: false, retentionDays: 90 },默认关 + 进LIGHT_MODE_OFF(retentionDays不进——它是时长不是行为面);两个键都进 settings 白名单与 api.jsNESTED_FLAG_PATHS(#254 的教训:漏了后者 effective 会静默缺键)。启动期裁剪与reclaim都读装配后的 cfg(面板改了保留期也认),且默认关时启动期不动手(一张空表不该被每次启动扫一遍);dsh-mneme reclaim报告从两项变三项,入口语义一字未动。已知边界三条:无历史回填(只对此后发生的蒸馏生效,从记忆反推原文是编造)、从未蒸馏过的窗口不留记录(窗口太小已消费游标 / 插件当时关着 / 没触发过蒸馏 / LLM abort,是「留存范围 = 蒸馏范围」的推论而非丢数据)、不覆盖工具结果与 code 输出。回归锁六条 + 五条:幂等(删游标换实例重跑同窗口仍一行)、隐私过滤(四类注入 + reasoning + 工具输出绝不落盘,而渲染路径仍带工具输出)、未截断(超长提示记录里逐字全文、渲染里找不到头部标记)、裁剪不毁资产(原文 0 / 链接在 / 记忆数不变 /provenanceFor仍可查)、默认关零写入(写入调用计数为 0,连收集都不做)、进 reclaim dry-run 报告(报真实行数与列文本大小且一字节不改,执行只删窗口外原文),另加启动期裁剪读装配 cfg、白名单与 PUT 往返(含范围外整数被拒)、轻量模式只关enabled不动retentionDays。非目标:不嵌入、不进fuseRecall、不进注入、不做取代判定、不做 value histories 查询、不动 8000/300/500 三刀——这两张表是旁路索引,不是第二条检索路(接进召回是另一个决定,成本与召回质量都要重新测)。设计文档见 docs/DISTILL.md。 -
面板手动新增记忆(issue #386 第 5 条):面板此前只能读、改、删、导入,没有任何手写入口——想记一条只能切到对话里让模型调
memory_save,而模型看不见的东西(一次现场结论、一条口头约定)恰恰最需要手动落库。「更多操作」菜单新增「新增记忆…」,弹层收类型 / 标题 / 正文 / 重要性 / 标签,字段与memory_save工具对齐。类型下拉刻意不含document:那是 agent 注册长文档的指针行,铸造口唯一(registerDocument,#230),列出来只会让人点了才被拒。表单不放 scope / sensitivity:显式归属走详情抽屉那条路,它带「放宽可见性」二次确认;在新建表单里顺手盖章会绕开那道确认。新增后端路由POST /api/dsh-mneme/create,落库走service.saveWithDedupe——去重键仍是(type, title, scope, sensitivity),所以同类型同标题会并入既有条目而不是长出第二行,服务端回action: "created" | "merged"决定提示语说「已新建」还是「已并入」:两者对用户是不同的事实,都报「已保存」会让人以为自己新写了一条、实际旧行被改。三处校验比 standalone 的POST /memories收紧(口径向/update看齐):content要 trim 非空(面板是人工输入,空正文只可能是误操作)、document提前 400、scope/sensitivity 不接受。写入准入(write-admission.js)对面板路径不生效——它无会话身份即放行——但路由仍显式处理action === "denied"→ 422,免得日后面板带上会话身份时静默吞掉拒绝。回归锁六条进test/api-routes-gaps.test.js(①鉴权 401 且 GET 落 404;②六种 400 校验且库内零写入;③201 created 且source为panel;④200 merged 且count() === 1;⑤denied→ 422 且理由随行;⑥落库异常 500),面板侧另有一条test/client.test.js锁「入口真的挂在树上」(菜单项确实调setCreateOpen(true)、弹层确实被条件挂载、onCreated确实刷新列表并选中新行、请求体逐字只有 type/title/content/importance/tags、表单不得长出 scope/sensitivity 输入框、类型下拉不得含document)。评审期补的两处:成功路径不得重置busy——提示要停 900ms 才关窗,那段时间按钮若重新可点,第二次同 type+title 会落进同一行并由appendContent把正文再追加一遍,提示还会从「已新建」翻成「已并入」,一次误触就写坏一条记忆;因此整个弹层只允许错误路径重置busy,按钮文案也随之改成busy && !msg,免得写完了还写着「保存中…」。另把字段锁从「整包里包含这些词」改成在CreateDialog源码切片上匹配真实请求体写法(在整包上断言clientSource.includes("title")恒真,等于没锁)。两侧都做了变异检验:面板/后端逐条把锁打回坏状态都转红(含「菜单项变哑」「弹层摘掉」「成功路径放回 setBusy(false)」「按钮文案只看 busy」「表单长回 scope」「不 trim title」「打错路由」这类只有肉眼才能发现的退化)。 -
面板存储回收入口(issue #386 第 4 条):库文件只增不减,回收一直只能靠命令行(
dsh-mneme-maintenance)或独立服务的数据面,面板侧连「现在能收多少」都看不见。状态页新增「维护」分区与「存储回收」卡:挂载即 GET 一次预估,摆出文件体积、空闲页、本次会清掉多少条输入快照与多少条归档向量;改窗口后必须点「重新估算」,数字与输入框不一致时执行钮保持禁用——对不可逆动作来说,确认态里摆着另一个窗口的数字是最坏的一种错位。执行是两步确认:第一次点击只进确认态(换成仓库既有的实心红mneme-btndangerconfirm),确认态里把窗口与 VACUUM 状态一并锁死显示,第二次点击才发出请求,请求体逐字{dryRun:false, confirm:true, olderThanDays:Number(days), vacuum}。VACUUM 默认关:它会短暂独占数据库(别的进程此时写入会SQLITE_BUSY),属可选动作,默认开等于每次回收都替用户独占一次库。VACUUM 失败不得渲染成干净的成功:两个 UPDATE 各自提交且不可逆,VACUUM 只负责把文件缩回去,半成功态单独出文案说明「清理已完成、不可逆、重跑幂等」,并且无论成败都露出回执号(缺失时明写「未记录」)——不可逆动作没有回执,出事时无法对账。面板侧不使用window.confirm(宿主 webview 里不可靠),确认门是渲染出来的分支而不是阻塞弹窗。后端新增路由GET/POST /api/dsh-mneme/maintenance/reclaim,底下就是src/maintenance.js那一份与 CLI、standalone 共用的实现,未改动数据面(src/api-standalone.js、src/maintenance.js零改动):GET 恒为只读预估;POST 默认 dry-run,要真执行必须带dryRun:false+confirm:true,否则回 400confirm-required并附上那份 dry-run 报告(确认的依据就是报告里的数字,且报告必须描述「将要执行的那个动作」——带了vacuum就得看到会 VACUUM 的那份,否则用户照着「不 VACUUM」的估算按下去)。两个方法都要鉴权:这是不可逆内容丢弃的入口,不能有例外。窗口解析在两侧刻意不同口径:GET 是 query 字符串域,只收十进制整数字面量(/^\d+$/),不用Number()——它把空串、空数组、纯空格都化成 0,而 0 在这里的含义是「清掉全部快照」,最坏解释绝不能从一个畸形输入静默拿到;POST 收 JSON 数字域,字符串"3"、-1、3.5、true、{}、[]一律 400(NaN不在坏输入列表里:JSON 表达不了它,stringify会把它变成null,而null与缺省同义、落默认窗口)。边界值 0 两侧都合法。invalid-json与invalid-body分开报(前者查调用方序列化坏了,后者查把数组当 body 发了),不用会吞成{}的parseBody。报告不加包装层,字段就在顶层——面板与 standalone 两个客户端必须看到同一个字段路径。接线:createApi增第 9 个参数maintenance,src/index.js把实例构造前移到 webServer 块之前并同时传给面板与 standalone(构造晚了面板路由拿到null,功能「看起来加了」但每次都回 503;少传则如实报 503maintenance-unavailable而不是把undefined.reclaim落成崩溃,既有 14 处不传第 9 参的调用照旧工作)。回归锁 14 条进test/api-routes-gaps.test.js(鉴权、GET 只读且畸形窗口不落最坏解释、POST 默认 dry-run 且三个改库原语一个都没被调用、确认门回带同窗口报告、执行真清且行数不变只置列、非布尔 vacuum、invalid-json 与 invalid-body 分开、POST 数字域、缺依赖 503、抛错映射 500 且 logger 留住原始错误文本、非 GET/POST 落 404、报告无包装层、执行时 VACUUM 真的跑、确认报告复述调用方要的窗口),面板侧 8 条进test/client.test.js(32 个 i18n 键中英成对且分区真的渲染、路由是 in-process 那条而非数据面、请求体逐字含确认门且全卡只有一处 POST、无window.confirm、卡片真的挂在状态页维护分区下、成功态只在确认门另一侧且 stale 时禁用、VACUUM 失败与回执如实渲染、VACUUM 默认关且非 2xx 带出服务端 error 串且成功后复位确认态)。独立验证:第三方复核方独立重做了实现者报的全部 19 条后端与 12 条面板变异(全部 RED),并额外找出 5 条契约硬要求没被任何锁钉住的覆盖洞(最严重的是「VACUUM 默认关」——危险默认没有任何测试守住),已逐条补锁并同样做变异检验转红;另有一处诚实记录:去掉 GET 内层 catch 后只断言状态码抓不住(外层 catch 也兜 500),必须靠注入 logger 断言原始错误文本留住才算真锁住。评审期(PR #410)另修三处:①olderThanDays补上界MAX_RECLAIM_WINDOW_DAYS = 3650(与llmAudit.retentionDays、recallRetentionDays同值),GET 与 POST 两侧统一——整数校验挡不住量级,1e20也是整数,而maintenance.reclaim会拿它算now() - days * 86400000再toISOString(),超出Date有效范围就抛RangeError;GET 侧还要额外挡Number.isFinite,否则 400 位十进制字面量化成Infinity会被当成畸形输入静默落回默认窗口 7——调用方要一个巨大窗口,实际只清了 7 天外的东西,且报告上写着 7 看不出异样。②confirm-required分支里那份 dry-run 报告挪进try/catch:它在readBody的回调里计算,外层 try 那时已经退栈,抛出去就是一个没人接的 rejection——调用方拿到的是「一个字节都没回」,而不是执行路径给出的 500internal。③ 面板「未记录」文案改成「未记录(审计未启用或回执写入失败)」:writeReceipt返回 null 有两种成因(审计被关、或回执写库失败只 warn),旧文案把后者说成前者,而这行出现在不可逆操作的结果区,用户会照它去对账。面板同时补上本地窗口上界(超界直接提示窗口不合法,不发请求,免得用户把 400 看成网络故障)。补锁三条并逐条变异转红(GET/POST 两侧量级、confirm-required 抛错映射 500、文案必须同时点出两种成因)。
⚠️ 行为变更
-
dreamMaxTokens默认值维持 131072,并补上「调小反而更贵」的实测依据(#135 成本审计):审计的原始假设是「把默认从 131072 降到 8192~16384 能省 token」,实测否定了它,故默认值不动,只把结论与证据写进config.js注释与文档。三条依据:① 这个键是上限不是计费口径(流式按实际用量计费,与sleepMaxTokens同口径),压上限只在真撞顶时生效,而近 10 天 88 个成功轮的 output 中位 24022、73.9% 超过 16384;② 撞顶的后果不是「少烧一点」而是整轮失败(截断 →extractJsonArray得 null →no json array in llm output),且调度器只有ok轮才刷新基线(src/dream.js:905-915,刻意设计「失败必须能立刻重试」),下一次写入用同一批输入重跑;按 133 个有真实用量的成功轮逐轮折算净总量:cap=8192 +49.7%、cap=16384 +54.1%、cap=24576 +43.3%、cap=32768 +15.9%、cap=49152 +6.5%、98304 起为 0;③ 真正的大头它治不到——把「时间就近」配上的 133 次成功调用的决策正文(merge/update 的 content+title,剥离代码自动补的隐式 keep)按中英分估约 419,487 token,同一批调用输出合计 3,543,111 token,正文约占 11.8%(换算方法敏感性区间 8.2%~22.0%,逐轮中位 12.1%),其余约 88% 是推理与 JSON 骨架,病根是推理与正文共享max_tokens单一池子,能治它的是dreamReasoningEffort与模型选择。共享预算这件事在插件侧没有接口可分离:宿主(DSH)侧确有分离机制(DEFAULT_THINKING_BUDGETS、adjustMaxTokensForThinking()、clampThinkingBudgetToAnswerRoom()保证正文至少留 1024 token),但仅 Anthropic Claude 路径默认生效;chat-completions 网关(volcano-engine / deepseek / glm 均在此列)须由路由 profile 开compat.supportsThinkingTokenBudget并指定compat.thinkingTokenBudgetField(上游端点还须认这个字段,asar 注释明写 "Off by default"),且两者都是 profile 级配置、插件ctx.llm.stream()无法 per-request 传递——故本次只把这条约束与前置条件写进注释(含「需要旧行为的库显式设回 131072」,以及显式调小仍然允许、但需知悉净成本方向)。用户可见行为零变化(默认值与上限都没动)。 -
新增
dreamMaxMergePerRun(默认 40):单轮 merge 决策数量闸(#135 成本审计派生):merge 是巩固里唯一回吐正文的动作——每条 merge 都要求模型重新写出合并后的 content(实测 2113 条 merge 平均 550 字符/条),而 keep / archive / conflict 都不带正文,所以单轮 merge 数是「输出里正文占比」唯一可控的杠杆;此前 update(2) / create(5) / archive(8) 都有上限,唯独 merge 没有。形态与现有三道完全一致:全局信号、dreamSkipInvalid也不豁免、超限整单拒绝(run 记 failed)。默认 40 的校准依据(只读探针:全库 246 轮 / 近 10 天 88 轮dream_runs.decisions):每轮 merge 数全库 p50 7 / p90 15 / p99 28 / max 39,近 10 天 p50 9 / p90 20 / p99 39 / max 39 —— 40 即观测分布上沿(历史命中 0/246),与 archive 闸默认 8 恰为其 p90=max 是同一校准法:面向未来的尾险护栏,不是成本削减工具(现有三道闸在实测分布下命中率均为 0...
v0.8.15
🆕 新增
-
面板暴露后端已有能力(issue #386 第一批):三张只读卡补齐「后端已实现、面板未暴露」的信息盲区。① 状态页「库内一览」新增「Markdown 镜像」健康卡,读
GET /api/dsh-mneme/health展示mirror.status(ok/degraded/unknown)、未落盘提示、last_error三码(磁盘满 / 权限不足 / 同步失败)的人话映射与上次尝试/上次成功时间——面板侧与后端同为 fail-closed:请求失败或 unknown 一律显示「未知」,绝不显示「正常」(镜像写失败此前在库内完全静默);② 设置「连接与安全」新增「独立服务dsh-mneme-serve」卡(一行示例命令 + 复制,并提示它与外部访问 API 默认抢同一端口、令牌同源);③ 同组新增「MCP 服务dsh-mneme-mcp」卡(可复制的.mcp.json挂载片段,数据面走外部访问 API 的 Bearer)。三张卡纯展示,不做进程启停/守护/端口探测,不加配置键;示例命令必须带-p @modusensus/dsh-mneme——bin 名与包名不同,裸npx会去装一个不存在的同名包(test/client.test.js有字面量锁)。 -
记忆库落点检测告警(issue #368):
memoryDir落在宿主 profile 作用域(形如…/profiles/<名字>/…)时,启动打一条可操作告警——#218 末楼记录过 profile 被清理导致整库连带丢失的实际事故。判定是纯路径段匹配(跨平台分隔符、大小写不敏感,只认显式profiles段,不做「看起来像用户数据」的猜测:漏报可容忍、误报等于狼来了)。只检测不搬迁:不改行为、不动数据、不加配置键、不进 settings 白名单(路径配置不是行为开关,与documentDir同口径),存量用户零迁移。根 README 中英成对补「记忆库落点建议」节。 -
注入前判定
preInjectGate(issue #380,E12 D1 裁决落地,opt-in 默认关):每帧注入候选出池后、进入 system prompt 前,一次池级 LLM 调用判出「会向本次请求注入意见/立场」的记忆(判定协议与 E12 实验 protocol-preinject.txt 同源;E12 实测判定+过滤把谄媚率 26.9%→2.5%,且 MemGauge 三阶段考卷实测写入/巩固/检索预算三面对内容毒均无防线——这是唯一过预注册判据的机制)。两级语义复用 #254 writeAdmission:enabled观察档(判定完成落llm_audit_logs,真实 token 用量 + 被标记记忆 id,先看真实负载的意见占比分布)、enforce缓存命中帧真滤除(E12:判定+标记比过滤差 +17.6pp,判定结果绝不进模型上下文)。宿主注入渲染是同步回调 → 判定走 async prefetch + cache 同步消费(queryVectorCache 同款),冷缓存首帧降级为原样注入、下一轮同 key 渲染生效;判定失败原样注入 +degraded审计行(审计行无条件落,无默认模型路由时model_id落unknown占位),防线故障永不阻塞注入;连续降级熔断一个冷却期(判定器系统性坏掉时不逐帧花钱,到期自动恢复)。审查加固(#382 评审):判定与滤除移进service.injectCandidates的touchRecalled之前(与 strictScope 同纪律——留在调用方做时,被滤掉的记忆照样刷温时钟并落mode='inject'曝光账,反馈环会把闸门想压下的意见记忆重新顶上来),recall_runs的「实际注入集」随之只吃滤后集合;整池被标记时放弃本帧 enforce(fail-open:记忆正文可影响判定模型,一条指令型记忆不该让整个记忆块静默失明),形态即审计行的n_flagged == n_pool;clear()(dispose 钩子)后置 disposed、在飞判定不回写缓存;缓存命中刷新序(LRU)。pin 池(#249 逐字保真)与 graphHint 线索行豁免;候选映射m#<i>局部序号(E13b 实测 UUID 回显脆弱);缓存键 = 查询 + 候选id:内容集(轮换/新记忆/内容被就地修正即重判);llmAudit关闭时本闸不运行;lightMode 强制关(applyLightModePreset支持点分键)。白名单 + 计数锁 +2 + 文档(CONFIGURATION.md 新节)。
⚠️ 行为变更
dreamMergeGuard默认值false→true(issue #339 / E8):巩固 merge 护栏自 v0.8.13 引入时是 opt-in 默认关,现改为默认开——合并对象命中长保留类型(与 archive 护栏同表:preference/pattern/rejected_solution/constraint/pitfall)的 merge 决策整条跳过,dreamSkipInvalid(默认开)下被跳条目进dream_runs.skipped、run 记 degraded。依据是它引入时就已写明的机制:archive 护栏只挡 archive 不挡 merge,而 merge 的「更精炼摘要」恰是约束失真的主通道——E8 实测巩固损耗里 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉;同类压缩管线的实测也显示安全规则被同速压缩后一轮只剩 53%、五轮只剩 10%,且全程无报错信号。需要旧行为的库显式设dreamMergeGuard: false(面板可关)。
🐛 修复
- 面板
memory_runtime verify的假失败:验证跑到包内去找模型(#387):embedModelCacheDir的约定是「空 =~/.dsh/mneme/models」(README / CONFIGURATION.md / LOCAL_MODEL.md 同口径),local-embedder 与 reranker 也都按String(x ?? "").trim() || 默认解析;但tools.js的 verify 分支把这个值原样交给verifyPayload,而默认 engine 的if (cacheDir)让空串等同于「不设置env.cacheDir」——transformers.js 于是转去找包内相对路径,报错指向node_modules/@huggingface/transformers/models/Xenova/...。真实嵌入用的是用户目录那份(所以检索一切正常),只有面板验证在包内找,报出functional: false的假失败;CLI(scripts/mneme-runtime.mjs)自己解析了默认目录,于是同一份运行时「CLI 通过、面板失败」。修法是把解析收进src/runtime/verify.js的默认 engine(新增导出resolveModelCacheDir),让「验证与真实推理用同一份模型」成为模块自身的不变量——任何调用点传空值都不会再验错地方(收在调用方只能修掉这一处)。顺带补同一调用点的第二处信息缺失:verify 此前不传model,恒探默认小模型Xenova/bge-small-zh-v1.5(与localEmbedModel默认值同字面量,因此只有换过模型才暴露),用户换模型时那份结论不是他实际使用的那份,现在按config.localEmbedModel探(空值仍落默认,默认配置逐字节不变)。回归锁四处:默认 engine 省略 / 空串 / 纯空白 cacheDir 都落用户级默认目录、显式目录不被默认值顶掉、cacheDir与model沿线透传、以及走面板同一条路的端到端用例(memory_runtime {action:"verify"}→verifyPayload→ 默认 engine,假 transformers 入口记录它收到的模型与目录);兜底做变异检验(把resolveModelCacheDir还原成旧行为时三条用例变红)。 - 事务内创建的记忆实体抽取被静默丢弃(#372):
scheduleEntityExtraction在事务内直接 return(注释写 deferred,实际transaction()从不补跑),而自动蒸馏的全部写入都裹在service.transaction()里——session:*来源的记忆自 0.8.x 起 100% 抽不到实体(报告者库内数据:9-25 后 337 条自动蒸馏记忆 0 抽取 0 审计)。修法:事务内入队、COMMIT 成功后由事务本体补跑(回滚路径丢弃,绝不对已回滚的行抽取);saveWithDedupe的合并分支补触发抽取(此前并入的新内容永不进实体面;saveAttr按 (entity_id, attr_key) 先失活再插入,重抽幂等);writeAudit在 LLM 路由解析失败(modelId 为空)时对非 ok 状态补一条 warn——消灭 #108 同款「零实体零日志」盲区。报告者(lqs50)三条主发现全部对源码坐实;其建议的「finally 里补跑」已修正为仅 COMMIT 成功后补跑。
🧹 工程
-
code-scanning 依赖告警清零(
sharp0.35.4 → 0.35.5):GitHub code-scanning 上仅剩的一条 open 告警是sharp@0.35.4命中GHSA-wq5f-xc86-pv6w(CVE-2026-96889,severity high,修复版 0.35.5——随包把上游 librsvg 带到 2.63.2)。它在 mneme 里不是直接依赖,而是@huggingface/transformers声明的^0.35.4传递依赖(本地嵌入运行时构建面,npm 用户装不到)。只改 lockfile 不够:runtime-manifest.json把sharp与 23 个@img/sharp-*(libvips 1.3.3)连 tarball 与 sha512 一起钉死并分发给用户,而扫描器看不见 JSON 清单,漏洞版本会继续发出去。故 lockfile 与清单同步升到sharp 0.35.5+@img/sharp-* 0.35.5+ libvips1.3.4(同一^0.35.4区间内,无 API 变更)。可达性本身很低——mneme 只做文本嵌入与重排,没有任何sharp()调用(不解码 SVG),RCE 还需 glibc Linux 且要攻击者提供 SVG;修的理由是升级免费且清单面向最终用户。npm audit(含 dev 与--omit=dev双口径)0 vulnerabilities;全量测试 1561/1560 pass/0 fail/1 skip。 -
npm 包
files补发docs/,allowScripts对齐 lockfile:README 有 9 处相对链接指向docs/*.md,而files白名单不含docs——这些设计文档在 npm 页面全 404(GitHub 上正常)。白名单加入docs(8 个 md 随包发布)。同处清掉另一笔陈旧账:allowScripts还写着onnxruntime-node@1.24.3与sharp@0.34.5,实际锁文件是1.30.0/0.35.5,而 sharp 0.35.x 已不带 install 脚本——按1.30.0保留 onnxruntime 的 postinstall 白名单、删除 sharp 条目(#384 明确欠的后续)。
v0.8.14
🐛 修复
- strictScope 硬过滤被图召回绕过(#371,#17 A3 的图召回分支):
injectCandidates的硬过滤原本长在 entityRecall 图召回合并之前——entityRecall 本身不做 scope 门控,被过滤出局的显式他域行会被图召回重新打上graphHint带回候选池,graphInjectHint开启时作为 [检索线索] 行进入注入块(宿主自动注入与 standalone/context同受暴露;为 #370 端点做可达性评审时由 CodeRabbit 发现)。修法是把硬过滤移到图召回合并之后、pin 池之前——顺带保证越权行进不了 pin 池;A2 软加权语义(auto / 存量他者保留可见)不受影响。暴露面 =strictScope+entityRecallEnabled(graphInjectHint决定是否泄入注入文本)多开关同开,默认全关。回归锁配变异检验(还原旧过滤位置测试变红)。
🆕 新增
- standalone API
GET /context(issue #370,#363 承诺):把宿主注入管线开放为端点——Mneme-Bridge 这类网页端桥接可以删掉自建的 pins 启发式了。GET /context?q&topK&threshold&agent_scope&workspace_scope返回injectCandidates全语义(优先级分层 / 编码门控 / heat / scope 软加权 / hybrid 语义路 / pin 前置)+ 画像 + 规则的结构化聚合{profile, rules, items, pinnedCount},不含渲染文本(语言 / 时间前缀 / 热记忆是宿主会话概念,调用方自渲染)。与宿主注入的两点 HTTP 面差异:查询向量即时嵌入——首次调用即走语义路,不等异步 prefetch(宿主注入渲染必须同步);不做跨轮轮换(daemon 无会话状态,调用方自管)。threshold是 importance 阈值(默认 3,与/search的相似度 threshold 同名不同义);scope 两参全缺 = 不解析(软加权与 strictScope 硬过滤都不触发,与宿主会话身份取不到时同构),任一给出则缺维按 fail-closed。recall_runs 记账随injectCandidates内建(#217 曝光型访问事件,mode='inject'与检索命中同表分账)——宿主注入同样记,端点自动同口径。/search顺手补 scope 透传(searchMemories本就收参、8790 此前没有入口;不传参数行为逐字节不变);createStandaloneApi新增可选embedder注入(宿主与dsh-mneme-serve各传 semantic 装配的同一实例),缺省时/context降级规则 + BM25 档,嵌入失败同理绝不 500。宿主外部访问与 daemon 同享该路由;测试锁形状(四键)、空 q 规则档、threshold 语义、pin 前置、scope 硬过滤、嵌入接线与失败降级,daemon 侧全链路含 recall_runs 分账锁。 - daemon 向量检索(#365,#363):
dsh-mneme-serve的/search接入完整语义管线——embedder/reranker 装配与 boot 自动回填从index.js纯搬移至src/semantic.js(宿主与 daemon 共用同一份,调用时序契约原样;backfillMissingEmbeddings经 index.js barrel 再出口,测试调用方零改动),daemon 侧新增createVectorIndex接线与--embed参数:local(默认,自管 runtime/嵌入模型缺失时经provisionRuntimedownload 档自动取件,可用DSH_MNEME_RUNTIME_TARBALL_DIR/DSH_MNEME_RUNTIME_MIRROR换离线/镜像来源;失败降级关键词并打可操作日志)、ollama、openai(读宿主面板 vector-config)、off。向量轴有注入假 embedder 的回归锁;createServeRuntime因此转为 async、语义键默认值在daemonSemanticCfg逐键锚定 config.js。 - 独立服务 daemon(
dsh-mneme-serve,#363):mneme 现在能在 DSH 宿主之外常驻——src/serve.js的createServeRuntime用最小装配(store → settings → mirror → service → maintenance → standalone API,每步锚定 index.js 装配行号)把数据面跑成独立进程,第三方集成(网页端桥接等)不必为挂载记忆库而保持 DSH 开机。第一期刻意无 LLM:巩固(autoDream)与蒸馏结构性不在 daemon 内,这是与宿主「单写者」的机械保证,不靠用户自觉。token 与 DSH 面板/CLI 共用同一 kv 凭证,端口/主机解析链与外部访问一致;createStandaloneApi新增strictPort选项——daemon 的配置端口被占即报错退出而非顺延(第三方把 URL 写死,静默换端口等于坏),不传该选项的宿主旁路行为不变。/search照常落 recall_runs,第三方检索的复用统计不缺数。多进程共存(daemon 与宿主同库互写互读)有专门回归锁;已知限制(双进程去重竞态、镜像双写、版本偏斜)见 docs/DAEMON.md。 - heat 注入侧拆独立开关(
injectHeatEnabled,opt-in 默认关;issue #218):heat 乘进注入排序在真实年龄混合的库上会饿死老约束(对照实验里 importance-only 排序的注入遵从显著更优,现行量级表现为拟合参数),注入侧因此拆出独立开关:默认关时heatEnabled开启后热度字段 / sleep 保护 / 面板投影照常,但注入排序乘数恒 1(与 heat 关闭逐字节一致,平价锁);显式开启恢复「层内乘 heat」的旧序(老用户回滚口)。召回侧时钟(touchLastAccess)与 sleep 降级联判不受本键影响。开关纪律照旧:config schema 与 settings 白名单成对注册,旗标计数锁同步。
🧹 工程
- 发布准备脚本在 CRLF 检出上不再假成功(
scripts/release-prep.mjs):该脚本用/^(# Changelog\n\n)/匹配 CHANGELOG 文件头,而 Windows 检出是 CRLF——正则命中不了,replace退化成空操作,脚本却照样打印✓ … 占位节,git status里看不出任何异常(CI 跑在 ubuntu 是 LF,所以只有本机发版会中招,v0.8.13 那次即如此、最后靠人工补的占位节)。规则抽成dsh-mneme/scripts/changelog-prep.mjs的纯函数:行尾两种都吃、插入内容跟随原文件行尾、带 BOM 也认;匹配不上则如实回报header-not-found,入口**报错退出(exit 1)**而不是假打印成功。配 6 条回归测试(LF / CRLF / BOM / 幂等 / 回报契约 / detectEol)。 - standalone API 的
/status版本号不再硬编码:该端点此前返回模块内的字面量常量,发布时不会随package.json一起 bump——手上的 0.8.13 实例对外报的是0.7.12,第三方集成据它判版本会读到假数据。现改为复用version-check.js的PACKAGE_VERSION(运行时读package.json,宿主侧/api/dsh-mneme/status走的就是这条),测试断言同步改成锁PACKAGE_VERSION而非字面量——旧断言把常量钉死,正是「发布漏 bump 而 CI 全绿」放行的原因。
v0.8.13
🐛 修复
- 密钥 / PII 判据三处加固(
src/sensitive-scan.js,PR #356,均为合并后独立复审发现):① 回溯有上界——email的 local part 与connection_string的 scheme 都作用在含.的字符类上,缺上界时每个起点都要一路重扫到结尾才失败(O(n²)),而判据在写入路径上同步跑:实测 34KB 点分链 0.6 秒、120KB 对抗串 23.5 秒(email 18.6s + 连接串 4.1s),等于把写入卡死;加上界后同输入约 60ms,边界取 RFC 5321 给 local part 的 64。② 赋值型规则左边界不再排除_——环境变量名正是拿_当分隔符,原写法让DB_PASSWORD=/MY_API_KEY=/MYSQL_PASSWORD=这类「前缀_关键词」整类漏放(只有恰好落在行首的API_KEY=能中);放宽后 #332 的 26 条语料仍全绿,挡误杀的仍是那道占位符守卫。③ 身份证档补校验位(GB 11643 / ISO 7064 MOD 11-2)——原先只有银行卡档有 Luhn,18 位纯数字(订单号 / 内部编号)先被身份证规则命中,等不到银行卡那条的校验。三处都配了回归测试,并做过变异检验(改回原写法各自变红)。判据仍在sensitiveScanEnabled默认关之后,线上行为不变。 - strictScope 硬过滤被
entity:/attr:前缀检索绕过(#17 A3 的漏网分支;issue #357 / PR #358):searchMemories里这两条前缀路在函数入口就 return,而硬过滤写在后半段的融合池上——早返回的路根本走不到,于是显式标注为他者 scope 的记忆换这两个前缀就能原样读出,而且是满分返回(连 A2 的 ×0.5 降权都没有)。暴露面是scopeEnabled+strictScope+entitySearchEnabled三者同开(默认全关),而entity:正是 #24 图谱线在推的语法。修法:scope 闸抽成gateByScope()单一实现,融合池与两条前缀路三处共用,让「过滤点写在哪」不再漂移(同 #349 把阈值口径收进activeStoreSize()的理由);闸门插在topK截断之前(先 filter 后 slice)——反过来会让出局的候选占掉名额,topK=1且首位出局时直接返回空数组;同时闸门必须在touchRecalled之前,否则出局的行仍会被刷回温时钟、并在被动确认开启时 bump 关联边,命中反馈落到了调用方本不该看见的行上。strictScope关(默认)时逐字节不变;他 scope 行的 A2 软加权要不要一并补到这两条路,按 #339 的口径另行决定。
🆕 新增
- 写入边界的密钥 / PII 判据(
sensitiveScanEnabled,默认关;PR #354):写入准入(#254 第 1 级)此前只跑空白 / 噪声两类判据,密钥 / PII 那一档按设计留了注入点而没实现。现在补上src/sensitive-scan.js——纯确定性、零 LLM,先认形状再认关键词(赋值型规则带占位符守卫,所以「把 API key 放进环境变量」这类讨论句不报)。命中落审计metadata.deny.reason='sensitive'+kind(密钥 / PII 分档,便于先看分布再决定放行策略),审计位与 #332 定的形状一致。开关分层:本键只决定「这类判据参不参与」,命中之后是仅告警还是真拦仍由writeAdmission.enforce决定(默认仅告警、拦截 opt-in)。回归样本集(10 条密钥 + 4 条 PII 正样本、12 条负样本)原样跑真判据:正样本不漏、负样本不误杀。 - 能力说明补 scope 声明规则(
memory_save工具描述;PR #355):memory_save的workspace_scope/agent_scope是必填面,此前的指引只讲「该不该写」,没讲「写给谁看」。现在在memory_save的工具描述尾部补一条判断规则:只在记忆确实只属于一个 workspace / 一个 agent 时才标注,否则两个都留空(未标注 = 处处可见,是安全的默认)。写进工具描述而不是总则——它是单工具的判据,总则那五条讲的是「何时查 / 何时写 / 何时 no-op」,加第六条会把单工具语义抬成全局纪律。#249 第二批;注入时机(N0 能力说明 / N1 分池 / N2 尾声提醒)未动工。
🧹 工程
- 发版说明与流程收尾:CHANGELOG 条目去掉非公开单号与实验代号,并新增
### 贡献者 / Thanks小节;CONTRIBUTING 补两条发版惯例。
贡献者 / Thanks
- @heptaspirit — 写入边界的密钥 / PII 判据(PR #354)与
memory_save的 scope 声明指引(PR #355)。
v0.8.12
🐛 修复
- 巩固阈值基线跨重启持久化(issue #89 基线半边):#291 只把时间闸门
lastRunAt从dream_runs恢复了回来,阈值基线baseline{count,chars}仍是调度器闭包里的内存变量,进程重启归零。库里记忆远多于阈值(默认 10 条 / 5000 字符)时,重启后第一条写入(notifyWrite→dreamHook→maybeSchedule,更新类写入同样计入)就让overBase成立——阈值被整个绕过,直接开跑一整轮巩固。0.8.11 实测一天四次重启,其后第一轮巩固发生时窗口内只新增 6 / 7 / 0 / 8 条记忆(1427 / 1488 / 0 / 1509 字符),每轮约 16 万 tokens(consolidate ≈7.5 万 + 总览 ≈8.8 万,llm_audit_logs实测)。现在成功轮把「本轮结束后的活跃库规模」写进dream_runs.store_count/store_chars(新增两列,幂等迁移,两列同时有值或同时为 NULL),调度器构造时按run_type='auto'播种;认不出行(新库、升级后还没跑过成功轮、老行两列为 NULL)就退回零基线——不假装知道基线,行为与升级前逐字节一致。阈值口径抽成activeStoreSize()单一实现,shouldTrigger与落库共用:两处各写一份过滤条件的话,重启播种读到的基线会与运行期判定漂移(一个把 document 算进去、一个不算),而这种漂移只在重启后才显形。run 自报的基线与审计行是同一份数字,进程内刷新改用它,两条路径不再各算一次。 - 蒸馏 JSON 崩溃窗口的 salvage:实测 10-20% 的蒸馏窗口输出了
含记忆条目的完整 JSON 数组,只因中段一处语法错误(如杂散引号)被parseSummaryJsonResult
整窗拒收——失败路径的生产行为是游标不推进 + 温度 0 重试同文同错 = 该窗口记忆静默丢失,
无任何用户可见信号;再蒸馏场景(摘要再当输入)崩溃率翻倍。修复:解析失败时做括号配对
双遍扫描(字符串感知 + 盲扫,去重合并)截出完整的顶层对象逐个 parse,救活的条目走正常
白名单校验后照常写库、推进游标;审计metadata.json_salvaged: true留痕。截出 0 条不视为
「显式空数组」,窗口保持可重试;lib-smoke 的三类退化输入行为不变。 - 工具结果新旧两种形状都收(PR #353):蒸馏采集
tool/result事件只认旧形状(载荷与isError装在data.message.content的type: "tool-result"块上),而 DSH 0.2.x 起内核直接投递role: "tool"消息本身(content只有type: "text"块、isError挂在消息上)——本机 14 个 v4 会话实测tool/result1095/1095 全是新形状,工具输出与工具报错被整批静默丢弃。现在两种形状都收,下游textOf/isError/trim一律不动。感谢 @dustinmoon78。
🆕 新增
- 图谱锚定层 Activation Anchoring(issue #24 块1):在
entityRecallEnabled的「查询命中实体 → 挂联记忆进融合池」之上再加一层级联——命中实体作锚定种子(graphSeedCap封顶,默认 12),沿entity_relations按graphCascadeDepth(默认 2)向邻居扩散,邻居挂联的记忆以跳数配额权重(1:2:3-hop = 0.5/0.3/0.2)补位进池,恒低于种子直达分(attr 1.0 / relation 0.9)。级联 BFS 逐层懒取邻居(>1 跳真正可达),邻接查询双向绑定(方向写反的关系行不丢)。默认关=检索行为与 #219 单跳轴逐字节一致(关档连种子裁剪都不生效)。新增三键graphAnchoringEnabled/graphSeedCap/graphCascadeDepth注册进 feature_flags 白名单,面板可启停;lightMode 强制关。 - 边权重演化 Weight Evolution(issue #24 块2):
entity_relations加weight/source两列(幂等迁移,存量行按 manual 1.0 认账)——建边来源决定初值(manual/confirmed 1.0、llm 0.4、tag 0.3),LLM 抽取自动建的边标source='llm';演化走bumpRelationWeight(只加不减、封顶 1.0,单条UPDATE ... MIN(1.0, weight + ?)原子抬升,负/非有限增量在存储边界拒绝)。新增两键graphWeightEnabled/graphWeightDelta注册进 feature_flags 白名单,面板可启停;lightMode 强制关。触达侧接线见块4(被动确认)。 - 关联提示防幻觉 Injection Budget(issue #24 块3):图召回候选进注入时默认只参与排序、不改变注入块构成(
graphInjectHint关,保守档);开启后以固定句式「[检索线索]」标注、并在首个线索行前加引导语(链路信息非事实断言),条数由独立预算graphInjectBudget约束——既不受 document 预算约束、也不挤占maxItems槽位。线索行不进 pin 池(pin 会在保守档下把它放回块内,绕过开关)。新增两键注册进 feature_flags 白名单;lightMode 强制关。 - 被动确认 Passive Confirmation(issue #24 块4):把「人工认可/删除/忽略」的确认门槛收敛成例外管理——正常触达(记忆被召回/注入)即视为对挂联关系边的被动确认,
store.getRelationsByMemory反查关联边后bumpRelationWeight抬一格(只加不减、封顶 1.0,自激回路由封顶遏制)。graphPassiveConfirm与 heat 是独立闸门(heatEnabled=false时仍可演化边权),且受块2 总闸graphWeightEnabled约束——两键同开才生效,单开通道键绕不过「演化默认关」。仅异常路径暴露给用户复核的复核 UI 留后续块。新增键注册进 feature_flags 白名单;lightMode 强制关。 dreamMergeGuard(opt-in 默认关):巩固 merge 护栏——合并对象命中
长保留类型(与 archive 护栏同表:preference/pattern/rejected_solution/constraint/pitfall)的
merge 决策整条跳过。实测巩固损耗里 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉
(archive 护栏只挡 archive 不挡 merge,「更精炼的摘要」恰是约束失真的主通道)。
dreamSkipInvalid(默认开)时被跳条目进dream_runs.skipped、run 记 degraded;关闭时整单拒绝。
dream 与 sleep 两条链路同一判据;白名单 + 计数锁 +1。
scope
- A2 软加权补齐到注入通道:此前 ×0.5/×1.25 只作用于 searchMemories,
实测「explicit 标注 + 软档」的注入集与无标注逐条相同(80/80)——自动注入这个主泄露面上
软档形同虚设,而文档本就承诺「关闭时全部为软隔离」。现scopeEnabled开启且会话至少一维
可解析时,注入规则路比较器在层内数值积乘scopeMultiplier(priority 档位不动)、selectiveInject
相似度重排乘同乘数;未激活时乘 1,排序与改动前逐字节一致。strictScope 硬过滤保持在软加权
之前,与检索侧叠加顺序一致。
🧹 工程
- 基线持久化回归集 9 条,全量测试 1441 → 1450:无种子对照组与播种组同库同写入(一个仍触发、一个被拦下——锁的正是这个差异,否则播种被忽略时两条断言同真同假)、阈值照常累积(+10 条仍触发)、run 自报基线优先于现场计算(运行后再长 15 条仍不触发)、成功轮落库 / 失败轮不落、sleep 行不进 auto 基线、半个基线被拒、老库补列幂等、非法种子(半个 / 负数 / 非整数 / 非对象)一律按「没有基线」处理、收尾取证失败既不反噬审计行也不写假基线、
index.js接线源码锁。变异检验:把播种改成忽略种子(toBaseline(baselineSeed)恒为空)→ 2 条变红;把「成功轮落基线」分支短路 → 1 条变红。 - 宿主开发依赖对齐 0.2 线(PR #344):
peerDependencies自 #121 起就声明覆盖 0.2.x,devDependencies却停在^0.1.0-rc.6(0.x 语义 =>=0.1.0-rc.6 <0.2.0,永远装不到 0.2 线)——声明支持 0.2、CI 却从没跑过 0.2,而桌面端 DSH NEXT 内置内核已是 0.2.0-rc.1。开发依赖对齐 0.2.0-rc.1,lockfile 与 CI 真落在声明覆盖的版本上。 - brace-expansion 钉到 5.0.12(PR #345,CVE-2026-102276/102277/102278):osv-scanner 三条告警(两条 high 栈耗尽 DoS + 一条 medium 二次时间 DoS)全落在 devDependency 链的同一包上——实际打不着(不被仓库自身 import),但 lock 合规仍要清;overrides 抬 pin 号即清零,
npm audit双口径 0 vulnerabilities。 - 测试数口径收拢到共用模块(PR #346):双 README 的测试数以五种形状重复(1 徽章 + 4 条命令注释),而两条写入路径各存一份替换规则——release-prep 只覆盖徽章 URL、
badge:sync才覆盖全部,自动路径反倒成了漂移来源(v0.8.11 发版后徽章 1437、注释停在 1431);替换规则收进共用模块,两条路径单一实现,下次发版不再各改一半。 - CI 加宿主版本兼容腿(PR #348):新增
test-host-legacy作业,把peerDependencies声明的 0.1.x 两段(0.1.0-rc.8 / 0.1.7-rc.2)各真跑一遍全量测试——此前这两段一直「声明支持却没人验证」,与 #344 是同一类缺口的两侧。
贡献者 / Thanks
- @dustinmoon78 — 蒸馏采集兼容
role:"tool"形状(PR #353,新旧形状都收),修复新内核下工具输出与工具报错被整批静默丢弃。