上一个版本 v26091501 以来的全部变更(修复 / 新增 / 变更 / 测试)如下;更新说明与仓库 docs/CHANGELOG.md 同源。
2026-09-28
修复
- 会话格式 v4 下插件注入消息会让接收方那一轮整轮失败(issue #68,阻塞级):DSH 0.1.7-rc.2 起会话持久化走 format v4,每条消息的
source.kind必须由生产者自报(canonical 形态plugin:<包名>,校验在@deepseek-ai/dsh-session-format-v3-to-v4的 message-sources 模块,持久化侧同规则);v3 时代的「裸kind: 'plugin'+ 并列 plugin 字段」被显式拒绝:format v4 message requires a producer-owned source kind。插件有 4 处注入点仍在用旧写法——会话广播 / COI 通知(lib/coi/index.js的deliver())、COI 任务状态通知(lib/coi/scheduler.js的#deliver())、写冲突警告(lib/coi/ws-coord.js的userMessage())与工作区活动公告板(同文件)。报错发生在接收方会话认领该事件时,所以症状是"注入所在的那一轮整轮失败",而不是注入调用本身报错;又因为公告板需要活跃会话 ≥2、写冲突与 COI 通知靠事件触发,表现为"重启就好、过一阵复发"——重启只是清空了公告板基线 / 节流状态,并非修复。修复:新增lib/coi/source.js导出唯一常量PLUGIN_SOURCE_KIND = 'plugin:dsh-memory-evolve'(与宿主producerKind()对未知生产者的映射一致),4 处全部改用它并删掉plugin字段。读取侧无需改动:advisor用自己的kind: 'advisor',review.js的lastTurnWasMessage()判undefined || 'user',新 kind 的语义不变。 - 带
[summary:…]的条目在记忆 Tab 无法删除 / 编辑 / 归档,归档页也无法移回主记忆 / 删除(issue #59):根因是展示层剥了程序元数据、匹配层没跟上——buildMemoryFiles()下发前会剥掉[summary:…](渐进式披露,2026-08-17 引入)与[id:…](2026-08-11 引入),前端MemoryTabView把这份展示文本当 raw 回传;而findExactIndex()只对[id:…]免疫,peekExact()用includes()严格相等,归档侧ArchiveStore.removeExact()更是entries.indexOf()严格相等(连[id:…]都不免疫——启用 Git 同步后写入身份证的归档条目一律删不掉),promoteArchived()的includes子串又被[summary:…]从中间截断。同一类问题第二次发生(第一次是[id:…]),结论写进代码注释:展示层每剥一种新元数据,匹配层必须同步扩展。修复:lib/store.js新增并导出normalizeForMatch(entry) = stripEntrySummary(stripEntryId(entry));findExactIndex()改用它,并新增{ first }选项(主轨多条命中仍报歧义、保守拒绝;归档允许重复条目,取首条);peekExact()改走findExactIndex并返回磁盘原文(归档「先写后删」因此保留[id:…]/[summary:…],转正回主轨不丢元数据);ArchiveStore.remove()与removeExact()两侧统一归一化;promoteArchived()的hits与随后的archive.remove()必须同一套基准——只修hits不修remove会出现「转正成功但归档那条删不掉」→ 同一内容同时留在主轨与归档,且全程没有报错提示。 runGit()的 30s 网络超时实际不生效——超时只杀直接子进程,Promise 可能永不 settle(issue #69,阻塞级):git ls-remote/git fetch会再派生git-remote-https,它继承并持有 stdout/stderr 管道写端;Node 的'close'事件要等所有 stdio 句柄关闭才触发,而resolve()只挂在'close'上——超时回调虽然把git杀了,'close'却永不触发,于是 Promise 挂死。实测(本地 TCP 黑洞,比不可路由地址更确定):修复前 120 秒内 Promise 从未 settle;修复后 30022 ms 返回{ok:false, code:null}并在 stderr 末尾标注超时。影响不止"卡一次":同步在 systemd user timer 下周期运行,一次挂死就让调用方永远拿不到失败原因,node --test跑tests/sync-*.test.js也会整片挂住。修复三层一起做:①settle()单次落定——超时 / error / close 谁先到谁生效、后到的丢弃,否则会出现"超时返回失败、随后 close 又翻案成成功";② 超时主动 resolve 一个失败结果,不再等 close;③ 落定后killGitTree()尽力清理——detached进程组 +kill(-pid)连带孙进程(Windows 无进程组信号,退回child.kill()),并 destroy 两路管道让'close'得以触发。顺序上必须先落定、再清理:反过来的话 destroy 管道触发的'close'会抢在超时结果之前 resolve 成一个假的成功。NETWORK_TIMEOUT_MS/LOCAL_TIMEOUT_MS一并导出,供回归测试按真实阈值推进假定时器,避免测试另抄一份阈值后与实现漂移。- 内置技能同步:
skillDir哨兵显式落地 + 成功/失败都不再静默(issue #67):issue 报告的"第二层"(config.skillDir默认null未解析就传给syncBuiltinSkills()并抛 TypeError)经复测不成立——resolveConfig()自首个提交起就有config.skillDir = resolve(config.skillDir ?? join(homedir(), '.agents', 'skills')),而apply()是先resolveConfig()再installCoi(),调用点拿到的从来是字符串(实测resolveConfig({}).skillDir→C:\Users\…\.agents\skills;本机~/.agents/skills里五个内置技能俱全,memory-consolidate的落盘时间正是插件启动那一刻)。真正成立的是 #58 的第一层:调用点在installCoi()内、而installCoi由coiEnabled(默认 false)门控——默认配置下内置技能不会同步,这一层由同批的e73a28e一并解决(见下条)。本次修的是 #67 里仍然值得修的三点:① 调用点显式兜底config.skillDir ?? DEFAULT_SKILL_DIR(新增导出常量,与resolveConfig()的解析结果由测试钉成一致),syncBuiltinSkills()则对非字符串入参抛看得懂的错(点名参数 + 指出该用resolveConfig()还是DEFAULT_SKILL_DIR),不再把 Node 的The "path" argument must be of type string抛给上层;② 每次同步都落一行可见日志(目标目录 + 总数 + 更新数),action: 'missing'(插件包内缺该技能 = 打包事故)单独 warn,catch 打完整堆栈——此前成功路径只在有变更时 log、失败路径只有一行 warn,"技能从来没装上"只能靠反查技能目录的 mtime 才能发现;③ 顺带修掉同一路径上一个 Windows 专属缺陷:skillVersion()的^---\n只认 LF,而 Windows 上core.autocrlf=true检出的 SKILL.md 是 CRLF——源与目标都解析出 0,版本门控退化成"永远相等",x-version 升级在 Windows 上静默失效(首次安装因目标不存在仍会复制,所以只有"升级不生效"这一半可见;本机~/.agents/skills里那五个技能正是这么来的)。改为^---\r?\n…后 CRLF 与 LF 解析一致。同源的normalizeSkillText()在 CRLF 下也把已有 frontmatter 误判成"没有"并叠加第二份头,一并改为按trim()比较边界行(不重写用户文本的换行)。 - 内置技能同步被 COI 开关连带关闭:默认配置下内置技能永远装不上(issue #58):
syncBuiltinSkills()的调用点原本在installCoi()内,而installCoi由coiEnabled门控(默认false——本插件本职是记忆/待办/技能,调度是按需增强),于是memory-consolidate与 kimi/codex/grok/hermes 四个 CLI 使用指南在默认配置下永远不会进技能库;调用点又包着 try/catch 只打一行 warn,连"从来没跑过"都留不下痕迹(与 §7.5 broadcast 当初"独立子模块挂在 COI 下拆不开"是同款事故)。修复:同步提到插件主装配(lib/index.js的apply()步骤 6.5),实现抽成lib/coi/skills-sync.js的syncBuiltinSkillsIfEnabled(config, pluginSkillsDir?)——判定只看coiSyncSkills,代码里不再出现coiEnabled;PLUGIN_SKILLS_DIR一并移过去并导出(供测试断言源头确实在包内),installCoi()内的调用与模块级常量删除,该模块只剩适配器指南的写入需要normalizeSkillText。coiSyncSkills的语义随之明确为"启动时是否把内置技能同步到技能库",与调度开关无关。 - 收尾的 key 建议没写明用哪个工具(issue #58 第二项):
snap.keyDuty/snap.subagentKeyTail只说"另向 target=key 提交 1 条建议",而memory_suggest的 target 白名单是['memory','user','todo-life','todo-work','todo-project','todo-daily'](lib/review.js),不含 key——照字面选工具即报"不支持 target=key",会话收尾会反复踩。走待确认队列的正规通道是 memory 工具的add(与memory_suggest共用enqueueSuggestion队列)。两处文案改为"另用 memory 工具 action=add 向 target=key 提交 1 条建议(走待确认队列,用户确认后写入并注入)",中英同步。
变更
- 内置技能
memory-consolidate升x-version: 2(外部 PR #58 第三项,按原文摘取):补四类实战边界——① 同一轨的写操作串行执行(同一个.md是读-改-写,交错执行会互相覆盖、也会撞上 drift guard;一个簇处理完再动下一个);②replace是整条替换:content必须给完整新全文,只传增量或新句子会静默吞掉其余段落(2026-09-16 实证:更正一条多段记忆时只写新句子,其余段落整条丢失,靠 memories 仓库的 git 备份才找回;改写前先用list、key 轨可用expand把原文读全);③ key 建议用 memory 工具add(target=key)提交,不要用memory_suggest(后者 target 白名单不含 key,传入即报「不支持 target=key」);④ 新增「四、故障处理(异常兜底)」——drift guard 报「无法解析往返」拒写时的逃生路径(先备份 → 用插件自带lib/store.js做serializeEntries(parseEntries(text))规范化并校验往返条数不变 → 回到工具流程),以及「判断条目是否粘连只认parseEntries(text).length」(用正则数\n§\n会因正文里逐字引用的内容性§假阳性)。技能正文取自 PR #58 的d78043d,与该提交逐字节一致。注意:这一项只有在本版之后才真正装得上——此前 CRLF 检出下skillVersion()解析出 0、版本门控退化成"永远相等",Windows 上 x-version 升级是静默失效的(见上)。
测试
- 新增
tests/injection-source-kind.test.js(4 例):① 复刻宿主 v4 准入规则并做前提自检——该规则必须拒绝退役写法、且通过 canonical 写法,否则后面的断言都是假绿;②PLUGIN_SOURCE_KIND的形状与准入;③ 扫描lib/不得残留退役写法;④ 扫描四处注入点必须引用同一常量(防未来新增注入点时又手写一份 kind)。行为侧证据落在既有用例上:tests/coi.test.js(会话广播 / 房间动态 / COI 完成通知)与tests/ws-coord.test.js(写冲突additionalContexts/ 公告板)直接断言真实构造出来的消息对象满足source.kind === PLUGIN_SOURCE_KIND且source.plugin === undefined;tests/plugin.test.js与两个 advisor 用例里按旧写法构造的桩数据同步更新。 - 新增
tests/store-display-match.test.js(12 例)。关键设计:展示文本取自真实的buildMemoryFiles(),测试里没有复刻一遍剥离逻辑——展示层将来再剥一种新元数据时,这些用例会跟着一起红,而不是"测试全绿、线上仍坏"。覆盖:normalizeForMatch只剥头部(正文里的[summary:…]字面量不动,且两条只差该字面量的条目仍分得清);主轨删除 / 编辑(时间戳与摘要原样保留)/ 分支范围 /[dsh-only]四个精确操作;peekExact返回磁盘原文;归档页删除(带 summary 与只有[id:…]两种,后者单独钉一条);归档重复条目取首条;promoteArchived转正后断言归档文件已清空(防"只修 hits 不修 remove"的半吊子修复);以及"归一化后多条命中 → 保守拒绝、一条都不动"。已验证:把normalizeForMatch退回旧的"只 stripEntryId"行为后,12 例中 5 例必失败。 - 新增
tests/sync-git-timeout.test.js(7 例,其中 1 例 POSIX 专属):注入永不 emit'close'的假子进程(等价于"孙进程占着管道"),用mock.timers按真实阈值推进,断言「未到点不得 settle」「到点必须 settle 且带超时说明与命令行」「迟到的'close'不得把结果翻案成成功」「两路管道被 destroy」「正常路径与 spawn 报错不受影响」。为避免回归退化成挂死——旧实现下 Promise 永不 settle,直接await会让测试进程一直挂着而不是报错——等待改用setImmediate+Date.now()轮询的真实时间预算,超预算直接抛出明确断言。另加一条 POSIX 专属用例:真实 detached 子进程 + 真实孙进程(正是git → git-remote-https的形态),验证kill(-pid)把整棵树清掉;Windows 没有进程组信号(也没有 SIGKILL 语义),跳过——Windows 上的保证是"调用方不再被拖住",孙进程可能活到它自己超时。已验证:把超时回调退回旧的"只 kill 不落定",7 例中 4 例必失败,且是明确的断言失败而非挂起。 - 新增
tests/skills-sync-default-dir.test.js(6 例):①DEFAULT_SKILL_DIR与resolveConfig({}).skillDir、resolveConfig({ skillDir: null }).skillDir必须一致(哨兵解析只有一处权威,防两处定义漂移),显式配置不被覆盖;②syncBuiltinSkills(ps, null | undefined | '')抛的错必须点名userSkillsDir、指出正确做法,且不含 Node 的The "path" argument must be of type string;③ 打包守卫——BUILTIN_SKILLS每一项都必须真的在插件包skills/里且有含 name/description 的 frontmatter(把运行期静默的action: 'missing'变成红测试);④ 用插件包真实内容同步:五项全部落盘、零missing、二次全unchanged;⑤ CRLF 下 x-version 升级必须生效(把新版正则退回 LF-only 后该例必失败——已验证);⑥normalizeSkillText在 CRLF 下不叠加第二份 frontmatter,缺字段与未闭合仍要报错。 - 新增
tests/skills-sync-decoupled.test.js(8 例):源头在包内(PLUGIN_SKILLS_DIR指向仓库skills/且五项齐全);coiEnabled:false不影响同步(issue #58 的要害);coiSyncSkills:false是唯一关断开关(coiEnabled:true也照样不同步,且连技能库目录都不创建);skillDir哨兵落到DEFAULT_SKILL_DIR(用空的内置源头避免写入真实技能库,目标目录改由日志行断言);同步抛错收口成一条带原因的 warn 并返回 null;每次启动都汇报结果(含最常见的"共 5 个,更新 0 个");源头缺失时逐个记missing并 warn;目标路径被同名文件占位时同样收口不抛。 tests/plugin.test.js新增两条装配级用例(跑真实apply()):coiEnabled:false时五个内置技能必须落盘、coiSyncSkills:false时连技能库目录都不创建。已验证「摘掉主装配调用」后第一条必失败。同文件的快照用例补上第二项的断言:key 建议必须点名memory 工具 action=add,且不得出现memory_suggest target=key(防止文案退化回"没写工具名")。
本次发布包含的提交(v26091501..v26092801)
2a74c58a167b chore: 移除误提交的 .dsh/ 测试残留 + 加入 .gitignoreba02abddde6d docs(changelog): 修正 #67 条目里「#58 第一层本次未动」的过时表述ab1b641cbcf7 feat(skills): 摘取 PR #58 的 memory-consolidate v2(x-version 1 → 2)e73a28e5a772 fix(skills): issue #58 内置技能同步与 COI 解耦 + 收尾 key 建议写明工具名1e931fb718bd fix(skills): issue #67 skillDir 哨兵显式落地 + 同步不再静默 + CRLF 版本门控ed7529b0e1a8 fix(sync): issue #69 runGit 超时主动 settle + 杀进程组,不再挂死dd206f6eb105 fix(memory): issue #59 匹配层对展示层剥离的 [summary:…] 也免疫f48ab4d0ff5a fix(coi): issue #68 注入消息改用 v4 producer-owned source kind
验证
- 本次新增回归 39 例:
injection-source-kind4、store-display-match12、sync-git-timeout7(含 POSIX 专属 1 例,Windows 跳过)、skills-sync-default-dir6、skills-sync-decoupled8、tests/plugin.test.js装配级 2。 - 三处关键回归都做过反向验证(还原修复后必失败):
normalizeForMatch退回旧行为 → 12 例中 5 例失败;runGit超时退回旧实现 → 7 例中 4 例失败、且是明确断言而非挂起;摘掉主装配的同步调用 → 装配级用例失败。 - 全量测试 868 例(上一版基线 829 + 新增 39)。在 Windows 开发机上失败 54 例,失败集与未修改的
v26091501基线逐条一致——均为 Windows 环境相关用例(lib/update.js的 git 夹具、darwin分支、沙箱敏感路径、假 git 的 PATH shim)与 3 个 advisor 系 flaky 用例,非本次引入。
提示:宿主需重启才会加载本次修复(
lib/*.js在进程启动时加载)。