Repository navigation
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 变红。