Skip to content

Releases: slow-stack/mneme

v0.9.4

Choose a tag to compare

@github-actions github-actions released this 21 Oct 14:30
df052de

🆕 新增

  • 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

Choose a tag to compare

@github-actions github-actions released this 19 Oct 19:17
81b47a1

🆕 新增

  • 沉淀链接线:把「会自动沉淀工作流」从一句话变成一条能跑的路(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

Choose a tag to compare

@github-actions github-actions released this 19 Oct 05:35

🆕 新增

  • 功能开关的第三态「待重启」:把「面板显示已改 ≠ 运行时已改」这个落差摆到界面上(#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 次成功,而用 sleepMaxTokens 8192 的睡眠相位和硬编码 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 实测踩中)。

    细节

    #457 曾以内联 opacity:1 覆写修复,但悬停变红仍走 .mneme-set-del:hover,连带常点亮了规则区的删除按钮,被 #459 回滚。本修复改用独立类 mneme-persona-del(常显、悬停变红),不触碰 mneme-set-del,规则区行为分毫不动;回归锁直接钉死卡删除按钮的 className 值,防 #457 式修法回归。

📚 文档

  • 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

Choose a tag to compare

@github-actions github-actions released this 18 Oct 13:32
20765db

🆕 新增

  • 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 同构。F3 rework-path 的两种成因(必经步骤被跳 / 被绕开成常态)共用一句问法,跳过场景答非所问,改为提议带 cause 字段、nudge 按成因选封闭问法、旧形状退回原问法。

  • candidates.js 的每一个换行都是「双 CR + LF」,从出生一路进了 main 又发到了 npm — 各 454 处。\r 在 JS 里是合法空白,解析、加载、1869 条测试全绿——当时没有任何检查会红。

    细节

    代价落在别处:任何改动这个文件的人 diff 都是整文件 908/908,噪声盖住真改动;同一份文件不同工具数出行数不同(Get-Content 908 行、编辑器 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 双向比对——写进文档的路由清单从此不再可能悄悄漂移。

🔐 安全

  • 外部访问 API 的鉴权从「每个 handler 自己记得写」变成「路由注册的必填字段」(#462 / #464) — 审计前的实际形状是 39 条路由里 20 条有 `requi...
Read more

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 23:32
94d855e

🆕 新增

  • 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=<绝对路径>,不是绝对路径就回 400 project-not-absolute:忽略它比拒绝危险得多——忽略会静默退成「全局」,而全局看起来一切正常。面板第一屏从「一张图」改成清单 + 图同屏(清单当选择器:名字 + 作用域 + 步数/闸门数/对照覆盖率 + 状态计数;画布在下面,默认展开第一条,点别的条目只是换图)——中间那一版做成「清单 ⟷ 图」两屏来回切,结果画布藏在一次点击后面、界面上没有任何提示,反馈连着两次说「看不到画布和节点」;作用域标签只取目录名(projectLabel()),绝对路径里有本机用户名,而面板是会被截图的那一面。两处必须拆成两个组件的地方:清单与图不能在一个组件里按 openId 分支——hook 不能条件调用,那样写会在点开那一下炸成「Rendered more hooks than during the previous render」,运行期才看得见;投影缓存也按项目 + 按工作流分桶,否则同一个进程里开着两个项目时会把 A 项目的清单端给 B 项目看,而画面看不出错。回归锁:test/pipeline.test.js 14 → 18(清单行自带规模与作用域、同 id 项目层赢且报出遮蔽、项目层坏文件不拖死全局层、不给项目根就只读全局层),其中原来那条「pipelines/ 必须进 package.json 的 files」原地反转成「不许在包里」——两头都防:进包则装完之后每个项目都能读到它,「项目级」又悄悄退回全局级,而这一幕在任何一次本地测试里都看不出来(测试读的是仓库目录);test/client.test.js 三条既有锁按新签名改写,并新增「第一屏是清单不是图」。全量 npm test 1796 / 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 条运行记录全是常量。这一刀补上读侧与数据面,并把...

Read more

v0.8.16

Choose a tag to compare

@github-actions github-actions released this 12 Oct 11:14

🆕 新增

  • 面板「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.js NESTED_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,否则回 400 confirm-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;少传则如实报 503 maintenance-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——调用方拿到的是「一个字节都没回」,而不是执行路径给出的 500 internal。③ 面板「未记录」文案改成「未记录(审计未启用或回执写入失败)」: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...

Read more

v0.8.15

Choose a tag to compare

@github-actions github-actions released this 09 Oct 19:24
7545221

🆕 新增

  • 面板暴露后端已有能力(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 依赖告警清零(sharp 0.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 + libvips 1.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

Choose a tag to compare

@github-actions github-actions released this 06 Oct 08:04

🐛 修复

  • 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/嵌入模型缺失时经 provisionRuntime download 档自动取件,可用 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

Choose a tag to compare

@github-actions github-actions released this 03 Oct 16:44

🐛 修复

  • 密钥 / 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

Choose a tag to compare

@github-actions github-actions released this 01 Oct 09:44

🐛 修复

  • 巩固阈值基线跨重启持久化(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/result 1095/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,新旧形状都收),修复新内核下工具输出与工具报错被整批静默丢弃。