Repository navigation
Releases: gezi-wen/sage-mem
Release list
v2.3.1
记忆星图的视角按钮顺序 —— 时间线在前、「星空」在后(产品要求;默认视角不变)。
行为变更
- 星图右上角的视角切换从「星空 | 时间线」改为「时间线 | 星空」。点开的仍是同一个「星空」视角,只是按钮位置变了。
- 默认视角没有改(仍是星空):它是这个功能的招牌视觉(README 封面图),而且改成时间线会让 8 条「画布真的画出来了 / 点得中暗星」的渲染断言失去前提。要改默认值得先给那些测试一条切模式的入口,是另一件事。
不变
星图的数据、布局、点击、回放、档案馆开关全部不变;wire 协议、typert 契约、remote 方法集不变。三屏界面快照(文件列表 / 自动做梦·设置 / 体检)哈希不变 —— 星图不在那三屏里。
新增测试
client-render-test +1:视角按钮顺序钉成断言(时间线 在前、星空 在后)。没有这条的话,有人换回去不会红。
npm test 4385 → 4386 passed / 0 failed。
实测
client-render-test465/0;界面快照守卫 6/0。- 离线评测:退出码 0,判定「无退化」。
v2.3.0
插件自带使用指南(随包技能)+ 注入文本现读上限 —— 装上插件就等于装上了「怎么用」的说明书。
新增
- 随包技能
skills/sage-mem/(SKILL.md+ 两个脚本):记忆长什么样、四类怎么分、什么不该存、frontmatter 硬格式与两个静默杀手、检索机制(词面重合而非语义)、索引与归档、体检与排错。cordis.patch.yml用@deepseek-ai/dsh-skill-filesystem注册本包自己的skills/(providerName: sage-mem、includeDefaultRoots: false,不碰宿主已有的技能根)。 - 注入文本带上限行:每次注入记忆时,正文前多一行「本轮上限(插件现读)」—— 一次最多几条、每条多少字、单会话累计多少 KB、单文件上限。数字来自
LIMIT_SPECS,技能里一个数字都不写,避免「技能写死 → 插件改了它不跟」的第二份真相。 - 两个随包脚本:
frontmatter-assert.cjs(记忆 frontmatter 断言)与memory-audit.cjs(索引断链、分类配比、整理阈值)。默认目录改读$SAGE_MEM_DIR,找不到时明确报错。
不变
wire 协议、typert 契约(39 条 invocation)、remote 方法集、界面全部不变。注入的检索结果一个字节都没动 —— 变的只是正文前多一行限额。11 份 golden 夹具已按新行为重新生成(有意改行为,不是夹具漂移)。
实测
npm test:4385 passed / 0 failed(4383 → 4385)。check-skill.js通过(177 行);npm pack --dry-run72 文件,skills/3 个文件进包;skills/脱敏扫描零命中。- 离线评测:退出码 0,判定「无退化」。
v2.2.2
文档补齐 —— 决策记录可导航、remote 清单落地、i18n 遗留清单纠错、历史文档标注冻结。零代码改动。
文档
docs/decisions.md:加目录(31 条一句话索引,并注明「编号不等于顺序」);给 D21 补「后续:见 D29」(它的「三个 Gateway」已被 D29 的第四个覆盖);新增 D32「加一条 remote 的清单」 —— 6 个文件 + 4 处硬编码条数 + 为什么 remote 面不在契约里 + 字段面守卫与错误形态的边界指路。- D31 的 i18n 遗留清单纠错:原来只写「记忆星图仍是中文」,实测
50-shared-ui.js(体检 / 注入调试 / 归档候选 / 保留名编辑)也仍是中文 —— D26 的清单里有50,D31 分批时漏掉了。现已补记。 docs/review-2026-10-31-response.md:顶部加「本文数字冻结于 2.1.21」,避免被当现状引用。
未做(有意)
README 双语正文行数对拍:现有 doc-consistency-test 只比 h2 / h3 / 表格行数 / 图片序列。试过加「正文非空行数」,但中英文天然有差(实测 4 节行数不同),整篇比较必误报;按节比较要引入新的节解析,收益不抵风险。维持结构级。
实测
npm test:4381 passed / 0 failed(与 2.2.1 相同 —— 纯文档)。- 离线评测:退出码 0,判定「无退化」。
v2.2.1
三处写路径收敛到同一个 prepareWrite,读方法拒绝统一带 .code,写方法拒绝统一回执,日志出口归一。
行为变更
- 写入口的「名字不合法」不再抛异常,改为回执
{ok:false, code:'invalid_name'}:writeFile/writeArchived/deleteFile三处。原来同一方法里「保留名回执、非法名抛异常」两种机制并存,而 JSDoc 写着「拒绝一律返回{ ok: false }」—— 那句话当时是假的。 - 读入口的异常现在带
.code:readFile/readRaw/readArchived/starmap/readFile的拒绝从裸Error变成Error & { code }。 - 客户端判「文件不存在」改为优先看结构化码:
isNotFoundError先看.code,文案正则只留兜底;probeRemoteExists同时认「信封外层的 code」与「reject 里的 code」。这条链控制一次写决策({create:true}是否降级成覆盖写)。 - 日志前缀归一:
autodream/engine.js手搓的第二套 logger 删除,改走warnLog;access.js的[sage-mem][debug]改为sage-mem:,且SAGE_MEM_DEBUG从模块级常量改成每次现读。
不变
wire 协议、typert 契约、remote 方法集、界面全部不变。三段写路径的拒绝矩阵没有变:保留名、影子文件、内容类型、体积、磁盘探针仍然逐一拒;变的只是顺序统一与「失败用哪种形态表达」。
修复
- 同一个坏输入从三个写入口进去会报三个不同的「第一个错」:内容类型检查原来排在 第 3 / 第 6 / 第 3 位。现在统一为
prepareWrite的固定顺序:保留名 → 名字合法 → 目录闸 → 影子文件 → 内容类型 → 体积 → 磁盘探针。 lib/index.js999 → 955 行(三处各约 30 行收敛成各约 12 行),组合根重新有了余量。
实测
npm test:4381 passed / 0 failed(本机)。client-render-test464/0;界面快照守卫 6/0。- 离线评测:退出码 0,判定「无退化」。
v2.2.0
业务错误码归一 —— 三套命名法收成一套 snake_case,并建唯一清单 + 守卫。
行为变更(破坏性)
15 个业务码改名(kebab / UPPER → snake_case):
not-found→not_found、move-failed→move_failed、not-archived→not_archived、target-exists→target_exists、delete-failed→delete_failed、stat-failed→stat_failed、invalid-name→invalid_name、lock-unknown→lock_unknown、restore-failed→restore_failed、lock-table-unknown→lock_table_unknown、not-file→not_file、too-big→too_big、not-dir→not_dir、UNVERIFIED_OVERWRITE→unverified_overwrite、FLOOD→flood。
同一概念不再有两个值:not_found(reader 侧)与 not-found(archive 侧)合并;too_big 与 too-big 合并。
谁受影响:直接读 result.code 的消费方。remote 方法名与 code 不在契约承诺内(契约 §7 把界面与接口方法名列为 UI 管道),但这是本插件第一次动 code 值,单列。
有意保留的三类码:Node errno(ENOENT / EEXIST / EIO …)、宿主的码(FS_SANDBOX_DENIED / FS_STALE_VERSION)、Error.code(SAGE_MEM_UNVERIFIED_OVERWRITE / LOCK_TABLE_UNKNOWN —— error.code 与 result.code 是两个命名空间,不合并)。
不变
wire 协议、typert 契约(39 条 invocation)、remote 方法集、界面、typert 类型名全部不变。客户端无需改动:它消费的码本来就是 snake_case。
新增
lib/shared/codes.js:30 个业务码的唯一清单 +isKnownCode()。test/code-registry-test.mjs(16 条,已登记):lib 里每个code:字面量必须在清单里、不许 kebab / 未登记 UPPER、清单键=值;白名单与清单不重叠;六条反向对照。
npm test 4365 → 4381 passed / 0 failed。
实测
- 归一扫描:43 处替换 / 10 个文件;复查 kebab 业务码零残留。
- 离线评测:退出码 0,判定「无退化」。
v2.1.25
契约守卫补上「字段面」—— 客户端 strict codec 与宿主 schema 逐条对表,补掉 5 处已漂的字段(其中一处是真 bug)。
行为变更
- 修复真 bug:
writeArchived失败分支会被判成坏结果。 宿主归档结果除ok外全部 optional(失败时没有file),客户端却把file声明成必填 —— 一次失败的归档会让客户端抛missing field file,把「归档失败」显示成「结果不合法」。现在按宿主archiveResultSchema对齐。 - 客户端 strict codec 补上宿主已有的字段:
fileSchema加tags/locked(少了它们,列表页的「按标签筛」与锁定按钮永远拿到undefined);archivedFileSchema加tags;starmapStarSchema加archived(+archivedAt/archivedReason可选);writeSchema加error;deleteSchema加error。 - 客户端在字段面上变严格了。宿主这侧这些字段本来就是必填(zod strict),正常响应不受影响;只有「宿主没发」时才会从「静默拿到 undefined」变成「明确报错」。
不变
wire 协议、typert 契约(39 条 invocation)、remote 方法集、界面全部不变。27 条故意宽松的 codec 保持原样 —— 「host 加字段不该让旧界面对整条调用报错」这个取舍没被推翻,只是让它们从「恰好没有 fields」变成「被识别为宽松」。
新增测试
test/client-exec-test.mjs 第 3 组新增字段面对表(+4 条):把客户端 codec 的 fields 与宿主 zod v4 的 _def.shape 逐条比较(含列表包装的元素面);带反向对照;并断言场上的宽松 codec 被识别出来(不是蒙对)。
npm test 4361 → 4365 passed / 0 failed。
实测
- 对表首跑就抓到 5 处漂移(
writeArchived/deleteArchived/memory/listFiles/listArchived/starmap/listStars),补完后 12 条 strict result 全覆盖、0 漂移。 client-render-test464/0;界面快照守卫 6/0(codec 改动不影响渲染)。- 离线评测:退出码 0,判定「无退化」。
v2.1.24
autodream 写路径的文件名判据收敛到唯一实现 —— a:b.md 这类 NTFS 交替数据流名字不再能写进记忆目录。
行为变更
- 写路径现在拒绝 7 类以前能过的名字:冒号(
a:b.md/MEMORY.md:evil.md—— NTFS 交替数据流,会写到看不见的隐藏流)、? * | < > "、控制字符。这是模型可达的路径(write_memory/report_contradiction)。崩溃形态不是报错,是「写进去了但资源管理器里看不见」。 - 写路径现在接受
a..md这类名字(以前被includes('..')误拒)。..在文件名中间是合法的,且已无路径分隔符,不构成穿越。 - 名字首尾空白仍会被去掉后判断(语义不变)。
不变
wire 协议、typert 契约、remote 方法集、界面全部不变。session-log.md / MEMORY.md 的放行行为不变。隐藏文件仍然不写(业务约束,不是文件名合法性)。
新增测试
test/shared-parity-test.mjs 第 1 组(+7 条):把 safeMemoryPath 作为第 5 个消费者纳入对拍 —— 断言「不比基准宽」「比基准严的项只有隐藏文件」,以及报告里的两个具体形状;两组反向对照证明判据会红。
npm test 4354 → 4361 passed / 0 failed。
实测
- 手工探针:
a.md收;a:b.md/MEMORY.md:evil.md/a?.md/a.pdf/.hidden.md拒;a..md/session-log.md收。 - 离线评测:退出码 0,判定「无退化」。
v2.1.23
修复界面快照守卫断链(它自 10-27 起就是红的,且入口文件根本不存在),补两条守卫,删掉从 2026-10-08 起从未执行的死代码。界面零变化 —— 三屏快照哈希与删除前逐字节相同。
修复
- 快照守卫断链:
test/_client-snapshot.mjs的文档注释指向不存在的test/client-snapshot-test.mjs,且它因_前缀被suite-registry-test的登记检查排除 —— 于是「3 屏全红」没有任何守卫能发现。现在入口名自洽、基线重建(承认 2.1.22 的界面状态),并在docs/release-checklist.md新增第 2.5 步强制每次发版跑一次。 - 死代码 78 行:删
client/13-starmap-panel.js的const inject+async function apply(该函数从不被返回、从未执行)与client/10-starmap-head.js的第二套 codec +TYPERT_REMOTE。
新增测试
suite-registry-test第 4 组(+3 条):辅助脚本文档里点名的test/*.mjs必须存在,带反向对照。client-scope-test第 5 组(+4 条):每个作用域内的顶层声明不许重名,实测覆盖 161 个声明(原 14 名单只覆盖 8.5%)。重点守function重名 —— 它不抛 SyntaxError,是静默覆盖。带同作用域 / 跨作用域两组反向对照。
npm test 4347 → 4354 passed / 0 failed。
不变
wire 协议、typert 契约(39 条 invocation)、remote 方法集、界面与文案全部不变。StarMap 的活渲染路径仍是 client/70-memory-panel.js 直接 h(StarmapRuntime.StarMap, { ctx: props.ctx })。
实测
- 三屏快照哈希与删代码前逐字节相同 —— 「只删不改」的机器证明。
- 快照守卫重建后 6 passed / 0 failed。
- 离线评测:退出码 0,判定「无退化」。
v2.1.22
修复
- 升级 sage-mem 后,如果 DSH 宿主进程没有真正退出(桌面版:关窗口 ≠ 退出,托盘常驻进程会继续活着),记忆面板里的写入会全部失败:
RemoteError: typert gateway: memory/writeFile: args fields do not match the descriptor: unexpected "opts"(issue #10)。
根因不在包内容:@deepseek-ai/dsh-typert-loader把每个包的 TYPERT manifest 按包名缓存、进程内不失效 —— 宿主进程里仍是升级前 2.1.1 的 strict descriptor(writeFile只有name/content),而浏览器侧已经是从磁盘现取的新客户端(第三参opts),于是参数在网关校验阶段就被挡掉,一次都没有落盘。 - 2.1.22 让写入不再被这个状态卡死:客户端识别这一种结构化失败(
gateway/arguments-invalid且点名unexpected "opts"),按旧协议安全降级重试,并在面板提示「本次未做版本校验」。 - 彻底退出 DSH(含托盘常驻进程)后重开即可恢复完整写协议(带
expectedRevision/create的版本校验)。
降级的安全边界(不是「出错就重试」)
{create:true}在旧宿主上没有对应语义:降级前先用readFile/readRaw探一次存在性,且只认明确的 not-found(ENOENT);目标已存在就明确报「同名文件已经存在」,不会覆盖。权限 / IO 读失败一律不降级;没有探针的 create 写同样不降级。- 其它网关错误(内容不合法、边界校验失败等)原样抛出,绝不重试;
stale_revision的「不自动重试覆盖」原则不变。
覆盖与验证
- 覆盖四个写入口:活动记忆保存、新建记忆(含「建立索引骨架」)、归档件原样保存、保留名(MEMORY.md / session-log.md)原样保存;中英双语文案同步。
- 真机验证(隔离 home + DSH 0.2.1-rc.5):陈旧宿主下新建成功并显示降级提示;同名新建被拒且文件 SHA-256 逐字节不变;重启宿主后正常路径只发一次带
opts的写;编辑路径照旧带expectedRevision。 npm test:4280 → 4347 passed / 0 failed;离线检索评测--baseline退出 0(无退化);wire 协议与 typert 契约(39 条 invocation)零改动。
升级建议
- 直接升级到 2.1.22;若你正停在 issue #10 的错误上,升级后彻底退出一次 DSH(任务栏托盘图标右键退出,确认没有残留进程)再打开,之后写入会走完整的版本校验。
v2.1.21
主题:文件列表的展开按钮接上业务回调 —— 点击不再抛 ReferenceError: setExpanded is not defined。
版本判定:2.1.21(PATCH)|理由:修「点活动卡片展开/收起直接报错」的界面缺陷;无新对外面、无契约变化|运行时源文件 3(client/72-files-view.js / client/70-memory-panel.js / client/50-shared-ui.js)|护栏内
行为变更
FilesView不再自己引用setExpanded(那个名字在拆分后属于父组件的闭包,早就不可见),改为接收业务回调onToggleExpanded(file);父组件用函数式 setState + 新的纯函数mergeExpanded(client/50-shared-ui.js)合并展开状态,避免读到旧闭包里的过期expanded。- 结果:点击活动卡片能在展开 / 收起之间切换(
data-clamp1 → 0 → 1),不会再抛未定义标识符。
不变
- 归档卡片的编辑 / 恢复 / 删除、筛选条、四种空态、悬浮与点外部关闭等交互路径不变;三屏 DOM 快照未重生成(本次没有改变静态结构)。
新增测试
test/review-211-q16-test.mjs(30 条,已登记npm test):用 VM 真渲染源组件后直接调Card.props.onToggle('a.md')—— 修前抛ReferenceError: setExpanded is not defined;修后断言展开状态真的翻转、再点一次收起、点 B 只展开 B;另加「快照式合并」的反向对照(旧写法会把另一条的展开状态丢掉)与一条定点no-undef判据(旧写法能被静态判据抓到)。
实测
- 修前红:11 passed / 16 failed(两条路径 —— 直调
Card.props.onToggle与真渲染后点击 —— 都抛同一个ReferenceError)。 - 修后:单跑 30/30;
npm test:4250 → 4280 passed / 0 failed。 - 边界:本次修的是行为(能展开);没有做真实浏览器截图走查(客户端测试是源组件 VM 执行)。