Skip to content

v0.9.2

Choose a tag to compare

@github-actions github-actions released this 19 Oct 05:35
· 30 commits to main since this release

🆕 新增

  • 功能开关的第三态「待重启」:把「面板显示已改 ≠ 运行时已改」这个落差摆到界面上(#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 变红。