@modusensus/dsh-mneme 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/bin/cli.mjs +61 -7
- package/docs/CONFIGURATION.md +23 -0
- package/lib/api.js +57 -2
- package/lib/client.js +72 -9
- package/lib/config.js +17 -0
- package/lib/dream/sleep.js +35 -12
- package/lib/dream.js +86 -14
- package/lib/embedding.js +52 -22
- package/lib/scope-state.js +277 -0
- package/lib/service.js +80 -3
- package/lib/settings.js +7 -0
- package/lib/store.js +139 -0
- package/lib/tools.js +71 -0
- package/package.json +1 -1
- package/src/api.js +57 -2
- package/src/config.js +17 -0
- package/src/dream/sleep.js +35 -12
- package/src/dream.js +86 -14
- package/src/embedding.js +52 -22
- package/src/scope-state.js +277 -0
- package/src/service.js +80 -3
- package/src/settings.js +7 -0
- package/src/store.js +139 -0
- package/src/tools.js +71 -0
- package/test/api.test.js +113 -1
- package/test/cli-flag-value.test.js +86 -0
- package/test/client.test.js +64 -3
- package/test/distill-tx-embed-gap.test.js +110 -0
- package/test/dream-maxtokens-fallback.test.js +132 -0
- package/test/embed-text-reasons.test.js +112 -0
- package/test/readme-claims.test.js +68 -0
- package/test/scope-state.test.js +481 -0
- package/test/tools-dto-schema-matrix.test.js +12 -3
- package/test/tools.test.js +6 -6
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/slow-stack/mneme)
|
|
9
9
|
[](https://github.com/slow-stack/mneme/actions)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
@@ -164,7 +164,7 @@ dsh web
|
|
|
164
164
|
|
|
165
165
|
### 语义增强(Semantic)🧠
|
|
166
166
|
|
|
167
|
-
v0.2
|
|
167
|
+
v0.2 起新增**可以完全离线运行的语义记忆引擎**(本地模型 + 精排 + 聚类):
|
|
168
168
|
|
|
169
169
|
- **本地 Embedding**:三后端可选——ONNX(`Xenova/bge-small-zh-v1.5`,离线)/ Ollama / OpenAI 兼容,失败自动逐级降级,最差回退关键词搜索
|
|
170
170
|
- **Rerank 精排**:`Xenova/bge-reranker-base` 对召回候选交叉编码精排,提升 Top-K 准确率
|
|
@@ -273,6 +273,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
273
273
|
|------|------|
|
|
274
274
|
| **v0.9.0**(2026-10-17) | M.O.(Modus Operandi · 手法)beta:工作流本体住 `<项目>/.mneme/pipelines/`、候选分层与升格判据、面板清单与图同屏(`actionPipelineEnabled` 默认关);设置面板折叠线挪到「新手之外」并按人话重写 22 个条目名(#427);安全与供应链加固(39 条路由声明式鉴权、OSV 有漏洞即失败、发布带 provenance) |
|
|
275
275
|
| **v0.9.1**(2026-10-18) | M.O. 从「看得见」跑到「跑得出账」:执行窗口由 `/mo-run start` / `/mo-run stop` 显式划开、运行账落 `<项目>/.mneme/pipelines/<id>.runs.jsonl`(账本坏了与从没跑过严格分开)、归属只出候选、观察旁挂与脱节检出、步骤 `assert` / `optional`、R1/R2 提议;沉淀链从真实会话日志挖 draft 工作流(判据恒 `null`,答案只能由人补);人格定义页 v1 → v1.2(按 agent 寻址多卡,默认关);面板、向量与检查更新的一圈修复(#448 / #450 / #460);Release 正文改分层渲染 |
|
|
276
|
+
| **v0.9.2**(2026-10-19) | 让「看不见的失败」显形:功能开关的第三态「待重启」把「面板显示已改 ≠ 运行时已改」摆到界面上(#481);作用域级可逆闸门让一路记忆能被单独按下与松开、不必改配置重启(#471,默认关);巩固用的 `dreamMaxTokens` 默认值撞服务商上限、八天 180 次全失败,面板现在能改、超上限还会自动降级重试(#478);CLI 把 `--` 开头的值当新旗标的真实缺陷(一次 1/4096 概率的 CI 红揪出来的)、人格卡删除 × 永远不可见、自动蒸馏写入的行 100% 拿不到向量(#477) |
|
|
276
277
|
| **v0.8.x** | 作用域隔离(agent / workspace 双维 + opt-in 硬过滤)、冲突队列人工裁决、生态化(stdio MCP server / 图召回轴 / 冷启动 / 注入截断) |
|
|
277
278
|
|
|
278
279
|
## 📦 安装
|
|
@@ -597,7 +598,7 @@ dsh-mneme-serve --embed off # 纯关键词 + BM
|
|
|
597
598
|
|
|
598
599
|
### MCP Server(任意 MCP 客户端接入)
|
|
599
600
|
|
|
600
|
-
> Claude Code / Codex / Hermes / OpenCode / OpenClaw 等各客户端的最小挂载配置速查表见[根 README](../README.md#用在其他-ai-工具里mcp)
|
|
601
|
+
> Claude Code / Codex / Hermes / OpenCode / OpenClaw 等各客户端的最小挂载配置速查表见[根 README](../README.md#用在其他-ai-工具里mcp);本节是完整配置与安全说明。独立分发包 [`mneme-memory`](https://www.npmjs.com/package/mneme-memory)(bin `mneme-mcp`)已发布,与插件内置的 `dsh-mneme-mcp` 并存、挂载命令互相兼容。
|
|
601
602
|
|
|
602
603
|
插件自带 stdio MCP server(`bin/dsh-mneme-mcp.mjs`,零依赖,随 npm 包发布,bin 名 `dsh-mneme-mcp`)。任何支持 Model Context Protocol 的客户端(Claude Code、Cursor 等)挂载后即可获得与 DSH 内一致的记忆工具六件套:`memory_save` / `memory_search` / `memory_list` / `memory_get` / `memory_update` / `memory_delete`——工具名、参数与去重合并、重要性等语义与 DSH 内工具对齐(测试锁漂移)。
|
|
603
604
|
|
|
@@ -674,7 +675,7 @@ src/
|
|
|
674
675
|
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
|
|
675
676
|
└── index.js # 插件接线
|
|
676
677
|
lib/ # src 的同步分发产物(npm run sync,prepack 钩子在发布时自动跑;一致性由 scripts/check-sync.js 在 CI 与 release.yml 校验——它只查不同步;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
|
|
677
|
-
test/ #
|
|
678
|
+
test/ # 2230 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
|
|
678
679
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单
|
|
679
680
|
```
|
|
680
681
|
|
|
@@ -683,7 +684,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
683
684
|
```bash
|
|
684
685
|
cd dsh-mneme
|
|
685
686
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
686
|
-
npm test # 运行
|
|
687
|
+
npm test # 运行 2230 个测试
|
|
687
688
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
688
689
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
689
690
|
```
|
|
@@ -728,7 +729,7 @@ dsh-mneme 的机制设计与下列公开研究相互印证(以下引用均逐
|
|
|
728
729
|
- **何时该忘 / scope 门控**:[PersistBench](https://arxiv.org/abs/2602.01146)(arXiv:2602.01146);呈现层缓解见 [Structured Memory](https://arxiv.org/abs/2608.08300)(arXiv:2608.08300)——scope 语义域门控的问题出处与零契约变更改法。
|
|
729
730
|
- **领域全景**:[Write–Manage–Read 综述](https://arxiv.org/abs/2603.07670)(arXiv:2603.07670)——「写入 / 管理 / 读取」三段式与插件整体形状互证。
|
|
730
731
|
- **数月尺度的可靠性工程**:[Memory as Infrastructure](https://arxiv.org/abs/2609.05510)(arXiv:2609.05510)——与 sha256 审计链、fail-safe 降级同属「记忆即基础设施」路线。
|
|
731
|
-
- **相邻系统(对照)**:[Zep](https://arxiv.org/abs/2501.13956)(时序知识图谱)、[Mem0](https://arxiv.org/abs/2504.19413)(生产级抽取式记忆)、[Hindsight](https://arxiv.org/abs/2512.12818)(retain / recall / reflect 三操作)走「写入时 LLM 抽取 + 向量/图检索」路线;dsh-mneme 的差异点在于:巩固由 LLM
|
|
732
|
+
- **相邻系统(对照)**:[Zep](https://arxiv.org/abs/2501.13956)(时序知识图谱)、[Mem0](https://arxiv.org/abs/2504.19413)(生产级抽取式记忆)、[Hindsight](https://arxiv.org/abs/2512.12818)(retain / recall / reflect 三操作)走「写入时 LLM 抽取 + 向量/图检索」路线;dsh-mneme 的差异点在于:巩固由 LLM 参与但产物全程留痕可回放,冲突冻结待人工复核,记忆只落本机。
|
|
732
733
|
|
|
733
734
|
## 📜 License
|
|
734
735
|
|
package/bin/cli.mjs
CHANGED
|
@@ -102,6 +102,40 @@ const USAGE = `${CLI_NAME} —— DSH 记忆插件(@modusensus/dsh-mneme)外
|
|
|
102
102
|
|
|
103
103
|
/* ---------------------------------- 参数解析 ---------------------------------- */
|
|
104
104
|
|
|
105
|
+
// `--flag value` 形态下,「下一个词以 -- 开头就别当值」这条规则有一个致命反例:
|
|
106
|
+
// 值本身就可能是 `--` 开头的字符串。外部 API 的 token 由
|
|
107
|
+
// `crypto.randomBytes(24).toString("base64url")` 生成(src/api-standalone.js),
|
|
108
|
+
// base64url 的字母表含 `-`,所以约 1/4096 的 token 头两位恰好都是 `-`:
|
|
109
|
+
//
|
|
110
|
+
// dsh-mneme reclaim --url http://127.0.0.1:55423 --token --UYoOHxr6PtE2-bfqW...
|
|
111
|
+
//
|
|
112
|
+
// 旧规则把 `--UYoOHxr6PtE2-bfqW...` 当成一个新旗标(键名 `uyoohxr6pte2-bfqw...`),
|
|
113
|
+
// 于是 token 解析成 `true`,flagValue 视作「没给」→ 退回环境变量/配置文件 → 全空
|
|
114
|
+
// → 报「未配置访问 token」。而 CLI 生成 token 的另一半同样会撞:`--token=--abc`
|
|
115
|
+
// 走等号分支没事,`--token --abc` 就废。这不是测试的随机性,是真实用户的随机性。
|
|
116
|
+
//
|
|
117
|
+
// 修法:区分「已知旗标」与「未知词」。下一个词以 `--` 开头时,只有它确实是一个
|
|
118
|
+
// 已注册的旗标才当作旗标,否则当作当前 flag 的值。于是 `--limit --json` 仍按
|
|
119
|
+
// 旧语义把 --json 当旗标,而 `--token --UYo...` 正确取到 token。
|
|
120
|
+
// 已知旗标表必须与下面 flagValue/flagBool 的实际用法保持同步;漏登记的代价是
|
|
121
|
+
// 该旗标被当成值吞掉,所以 test/cli-flag-value.test.js 用文本锁对齐两者。
|
|
122
|
+
const KNOWN_FLAGS = new Set([
|
|
123
|
+
// 取值的旗标
|
|
124
|
+
'url', 'token', 'type', 'title', 'content', 'importance', 'tags', 'source',
|
|
125
|
+
'limit', 'offset', 'mode', 'topk', 'top-k', 'min-importance', 'minimportance',
|
|
126
|
+
'older-than', 'olderthandays',
|
|
127
|
+
// 布尔旗标
|
|
128
|
+
'apply', 'vacuum', 'json', 'help', 'version'
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
/** `--foo=bar` / `--foo` 取出小写键名;非旗标词返回 null。 */
|
|
132
|
+
function flagKeyOf(word) {
|
|
133
|
+
if (typeof word !== 'string' || !word.startsWith('--') || word === '--') return null;
|
|
134
|
+
const key = word.slice(2);
|
|
135
|
+
const eq = key.indexOf('=');
|
|
136
|
+
return (eq >= 0 ? key.slice(0, eq) : key).toLowerCase();
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
function parseArgv(argv) {
|
|
106
140
|
const positionals = [];
|
|
107
141
|
const flags = {};
|
|
@@ -118,10 +152,14 @@ function parseArgv(argv) {
|
|
|
118
152
|
if (eq >= 0) {
|
|
119
153
|
value = key.slice(eq + 1);
|
|
120
154
|
key = key.slice(0, eq);
|
|
121
|
-
} else if (i + 1 < argv.length && !argv[i + 1].startsWith('--') && argv[i + 1] !== '--') {
|
|
122
|
-
value = argv[++i];
|
|
123
155
|
} else {
|
|
124
|
-
|
|
156
|
+
const next = argv[i + 1];
|
|
157
|
+
const nextIsFlag = next !== undefined && flagKeyOf(next) !== null && KNOWN_FLAGS.has(flagKeyOf(next));
|
|
158
|
+
if (next !== undefined && next !== '--' && !nextIsFlag) {
|
|
159
|
+
value = argv[++i];
|
|
160
|
+
} else {
|
|
161
|
+
value = true;
|
|
162
|
+
}
|
|
125
163
|
}
|
|
126
164
|
flags[key.toLowerCase()] = value;
|
|
127
165
|
} else {
|
|
@@ -691,7 +729,23 @@ async function main(argv) {
|
|
|
691
729
|
}
|
|
692
730
|
}
|
|
693
731
|
|
|
694
|
-
main
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
732
|
+
// 直接运行才进 main(import 只取 parseArgv 做单测时必须安静):npm 全局安装走
|
|
733
|
+
// node_modules/.bin 的符号链接,argv[1] 是链接路径而非真身,所以要 realpath 两侧
|
|
734
|
+
// 再比,不能用字符串相等。
|
|
735
|
+
const invokedDirectly = (() => {
|
|
736
|
+
if (process.argv[1] == null) return false;
|
|
737
|
+
try {
|
|
738
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
739
|
+
} catch {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
|
|
744
|
+
if (invokedDirectly) {
|
|
745
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
746
|
+
console.error(err?.stack ?? String(err));
|
|
747
|
+
process.exit(1);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export { parseArgv, KNOWN_FLAGS, flagValue, flagBool, flagKeyOf };
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -28,6 +28,29 @@
|
|
|
28
28
|
| `scopeEnabled` | `false` | 存储层总开关:**两条写入路径**(工具调用 `memory_save`/`memory_update`,以及机器提炼·会话蒸馏)都自动标注 agent_scope / workspace_scope(标注只贴**新建行**:并入既有行时不重贴,既有行的归属属于它原来被提炼出来的语境),去重键扩展;检索与注入排序按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见,issue #339 补齐注入通道) | opt-in;关 = 写入不标注、行为与 A1 前逐字节一致 |
|
|
29
29
|
| `strictScope` | `false` | 硬过滤模式(A3):他 scope 完全不可见、未标注恒可见(fail-closed);硬过滤先于软加权执行 | 依赖 `scopeEnabled` 打开才有意义;关 = A2 软隔离(他 scope 降权保留可见,检索与注入同权重) |
|
|
30
30
|
|
|
31
|
+
## 作用域级可逆闸门(issue #471)
|
|
32
|
+
|
|
33
|
+
上面两个键是**事实轴**——答「这条记忆属于谁」,改的是归属数据、跟着记忆行走。下面两个键是**政策轴**——答「这个作用域现在通不通」,由人按作用域整体按下、跨会话存续、松开即原样回来,**一条记忆行都不动、一个配置键都不改、不用重启**。两条轴落在同一个判定出口(`gateByScope`,全仓唯一的过滤实现),可叠加。
|
|
34
|
+
|
|
35
|
+
按下与松开走工具 `set_scope_state`(`dimension`: `agent`/`workspace`,`scope_value`: 标签本身或 `*` 表示该维度全局,`state`: `paused`/`active`)。状态存 `scope_state` 表,键 = 维度 + 作用域值,`UNIQUE(dimension, scope_value)` 保证幂等:**重复按下同一状态不刷新 `decided_at`**(同 `memory_archive` 的取舍——否则「最后一次真正变更」的时刻会被噪声盖掉)。
|
|
36
|
+
|
|
37
|
+
两级语义与 `writeAdmission` / `preInjectGate` 同构:先观察、再拦。
|
|
38
|
+
|
|
39
|
+
| 键 | 默认 | 作用 | 开启后果 / 冲突 |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| `scopeState.enabled` | `false` | 跑政策轴判定:被按下的作用域其候选**照常注入**,但每次落一条 `llm_audit_logs`(`trigger_source=scopeState`,`metadata.paused` 记被按下的是哪些作用域、`metadata.gated` 记本应拦下几条、`metadata.enforced=false`) | 关 = 判定不跑、行为与今天逐字段一致。先看「如果按下会少掉多少」的真实分布,再决定真按——`gated` 恒为 0 就说明这一路本来就没在供料 |
|
|
42
|
+
| `scopeState.enforce` | `false` | 真的不注入:被按下的作用域其候选出池(检索侧同款,`gateByScope` 一处生效;巩固与睡眠两侧的读入口与写入路径同款) | 关 = 仅观察(`metadata.enforced=false`)。**enforce 无 enabled 时无效果**(同 `preInjectGate`)。按下的是政策不是事实:记忆行、向量、归档位、作用域标注全部不动,松开即恢复 |
|
|
43
|
+
|
|
44
|
+
覆盖范围:`gateByScope` 的三条检索路(融合池、`entity:`、`attr:` 前缀路)与注入候选池(`injectCandidates` 内、图召回合并之后、pin 池之前)。
|
|
45
|
+
|
|
46
|
+
**不覆盖检视面**:`memory_list`、`memory_get`、面板列表**刻意不过这道闸**(它们走 `strictVisibility` 与 `store.list` 的 `visibility` 参数,是另一条平行的链)。按下之后人还要能查「我按下了什么、里面有什么」——检视面关掉,人就没有入口确认该不该松开。这也正是「按下的是政策不是事实」的具体含义:行还在,只是不再喂给模型。
|
|
47
|
+
|
|
48
|
+
**巩固(dream)侧另接四处**:`activeStoreSize`、叙述条 `inputs`、`runDream` 的读入口、总览 `summaryAll`——否则被按下的行会照常参与阈值判定与合并。**总览那一处只能从源头挡**:总览落库时是**无作用域**的 summary 行,而 `gateInjection` 只拦匹配暂停作用域的行,没有标注的行永远不会被它拦下;被按下的内容一旦进了总览就变成「洗干净的全局行」,此后一直注入。
|
|
49
|
+
|
|
50
|
+
**睡眠(sleep)侧另接四个相位**:冲突消解、归档降级、模式发现、关系补全的候选池。这一侧比日间更要紧,因为**它不只读**——`phaseDemotion` 会 `setArchived` 与 `demoteToSummary`(真改归档面),`phasePatterns` 会铸出 `type=pattern` 的新行;不接的话闸门开着而每 6 小时一次的睡眠照样在被按下的作用域上动手,「按下」就只等于让它在白天看不见。两处位置纪律写在注释里:降级的过滤放在循环之外(不是循环体里 `continue`,免得将来有人加副作用时漏掉),模式发现的过滤排在 `.slice(0, limit)` 之前(否则被按下的行照样占名额,它们既不会被扫、也不该挤掉别人)。
|
|
51
|
+
|
|
52
|
+
`llmAudit.enabled=false` 时观察档一行都不写(同 `writeAdmission` 的口径)。不进 lightMode 强制关闭清单——它只是查一张小表的判定,不引入重型后台路径、不加常驻文本(同 `actionPipelineEnabled` 的定性)。
|
|
53
|
+
|
|
31
54
|
## 审计
|
|
32
55
|
|
|
33
56
|
| 键 | 默认 | 作用 | 开启后果 / 冲突 |
|
package/lib/api.js
CHANGED
|
@@ -207,6 +207,15 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
207
207
|
// feature flags 快照(GET/PUT 共用):overrides 是持久化的用户显式覆盖;
|
|
208
208
|
// effective 是启动配置在白名单键上被 overrides 覆盖后的最终值。config 缺席
|
|
209
209
|
// (旧调用方直连、未传 cfg)时只报被覆盖的键,不把不存在的默认值编造给前端。
|
|
210
|
+
//
|
|
211
|
+
// pendingRestart 回答的是第三个问题:「我改的开关,现在真的生效了吗?」
|
|
212
|
+
// 功能开关的生效节奏与 panel_mode 相同——启动时由 index.js 合并进 cfg,运行中
|
|
213
|
+
// 那份 cfg 是冻结的(见 index.js 装配处的注释)。于是面板显示「已改」不等于
|
|
214
|
+
// 运行时已改:#481 实测人格总闸正是这样,面板开着、人格段却不注入,用户只能
|
|
215
|
+
// 靠反复试错发现。effective 表达的是「运行时取什么值」(overrides 优先),
|
|
216
|
+
// **反推不出待重启**,必须另比一次冻结值——这是本字段存在的唯一理由。
|
|
217
|
+
// 只报「显式覆盖 ≠ 冻结值」的键:未被覆盖的键运行时值就是 cfg 值,不存在落差;
|
|
218
|
+
// config 缺席(旧调用方直连、未传 cfg)时无从比较,返回空数组而不编造。
|
|
210
219
|
const flagKeys = [
|
|
211
220
|
...FEATURE_FLAG_SPEC.booleans,
|
|
212
221
|
...Object.keys(FEATURE_FLAG_SPEC.ints),
|
|
@@ -232,7 +241,11 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
232
241
|
// Issue #399:蒸馏原文留存 + 溯源。两个键都是嵌套子字段(enabled 开关 +
|
|
233
242
|
// retentionDays 整数),漏登记的后果同上——effective 静默缺键。
|
|
234
243
|
"distillRecords.enabled": ["distillRecords", "enabled"],
|
|
235
|
-
"distillRecords.retentionDays": ["distillRecords", "retentionDays"]
|
|
244
|
+
"distillRecords.retentionDays": ["distillRecords", "retentionDays"],
|
|
245
|
+
// Issue #471:政策轴——作用域级可逆闸门。两个键都是嵌套子字段,漏登记的后果
|
|
246
|
+
// 同上(effective 静默缺键、面板读不到也 PUT 不进)。
|
|
247
|
+
"scopeState.enabled": ["scopeState", "enabled"],
|
|
248
|
+
"scopeState.enforce": ["scopeState", "enforce"]
|
|
236
249
|
// sensitiveScanEnabled(#164 A2)是顶层扁平键,不需要进这张表——走 configFlagValue
|
|
237
250
|
// 的默认分支即可。
|
|
238
251
|
};
|
|
@@ -241,6 +254,19 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
241
254
|
if (path) return config?.[path[0]]?.[path[1]];
|
|
242
255
|
return config?.[key];
|
|
243
256
|
}
|
|
257
|
+
// 「显式覆盖 ≠ 冻结值」的键。用 Object.is 比而不是 !==:NaN 与 ±0 的边角上
|
|
258
|
+
// 两者会给出相反的结论(Object.is(NaN, NaN) 为真、Object.is(0, -0) 为假),
|
|
259
|
+
// 开关值里虽然不该出现它们,但比较语义与「值是否相同」对齐更省心。
|
|
260
|
+
// 未被覆盖的键不看:它的运行时值本来就取自 cfg,不存在落差。
|
|
261
|
+
function pendingRestartKeys(overrides) {
|
|
262
|
+
if (!config) return [];
|
|
263
|
+
const pending = [];
|
|
264
|
+
for (const key of flagKeys) {
|
|
265
|
+
if (overrides[key] === undefined) continue;
|
|
266
|
+
if (!Object.is(overrides[key], configFlagValue(key))) pending.push(key);
|
|
267
|
+
}
|
|
268
|
+
return pending;
|
|
269
|
+
}
|
|
244
270
|
function featureSnapshot() {
|
|
245
271
|
const overrides = settings.getFeatureFlags();
|
|
246
272
|
const effective = {};
|
|
@@ -251,7 +277,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
251
277
|
if (value !== undefined) effective[key] = value;
|
|
252
278
|
}
|
|
253
279
|
}
|
|
254
|
-
return { overrides, effective };
|
|
280
|
+
return { overrides, effective, pendingRestart: pendingRestartKeys(overrides) };
|
|
255
281
|
}
|
|
256
282
|
|
|
257
283
|
// Ensure the service has an embedder when the API layer was handed one
|
|
@@ -1054,6 +1080,25 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1054
1080
|
const configured = embedder ? (typeof embedder.configured === "boolean" ? embedder.configured : true) : null;
|
|
1055
1081
|
const embedded = Number(stats?.embeddedCount ?? 0);
|
|
1056
1082
|
const total = Number(stats?.totalCount ?? 0);
|
|
1083
|
+
// Per-model / per-dimension provenance of stored vectors. "embedded"
|
|
1084
|
+
// counts rows with any vector; rows whose dimension differs from the
|
|
1085
|
+
// current embedder output are skipped by searchVector, so the honest
|
|
1086
|
+
// retrievable number is the current-dimension slice. modelBreakdown
|
|
1087
|
+
// groups by row-level embedding_model ('' = unknown origin).
|
|
1088
|
+
const dimensionBreakdown = typeof service.embeddingDimensionBreakdown === "function"
|
|
1089
|
+
? service.embeddingDimensionBreakdown()
|
|
1090
|
+
: null;
|
|
1091
|
+
const currentDim = embedder?.dimension != null ? String(embedder.dimension) : null;
|
|
1092
|
+
const byDimension = dimensionBreakdown
|
|
1093
|
+
? [...dimensionBreakdown.entries()].map(([dim, c]) => ({ dimension: dim === "" ? null : Number(dim), count: c }))
|
|
1094
|
+
: [];
|
|
1095
|
+
const unsearchable = (currentDim && dimensionBreakdown)
|
|
1096
|
+
? [...dimensionBreakdown.entries()]
|
|
1097
|
+
.filter(([dim]) => dim !== currentDim)
|
|
1098
|
+
.reduce((s, [, c]) => s + c, 0)
|
|
1099
|
+
: 0; const modelBreakdown = typeof service.embeddingModelBreakdown === "function"
|
|
1100
|
+
? service.embeddingModelBreakdown().map((r) => ({ model: r.model === "" ? null : r.model, count: r.c }))
|
|
1101
|
+
: [];
|
|
1057
1102
|
sendJson(res, 200, {
|
|
1058
1103
|
ready,
|
|
1059
1104
|
configured,
|
|
@@ -1064,6 +1109,16 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1064
1109
|
: null,
|
|
1065
1110
|
// 空库时 ratio 必须是 null 而非 0 —— 0 会被读成「一条都没嵌上」。
|
|
1066
1111
|
coverage: { embedded, total, ratio: total > 0 ? embedded / total : null },
|
|
1112
|
+
// distill-tx-embed-gap 后续:向量「存在」≠「可检索」。unsearchable =
|
|
1113
|
+
// 维度与当前 embedder 输出不同的行数(searchVector 会跳过它们),
|
|
1114
|
+
// retrievable = 与当前维度同维、真正参与召回的行数。两者差值非零时
|
|
1115
|
+
// 面板必须能说出「有 N 条向量不可检索」,而不是只给一个虚高的 coverage。
|
|
1116
|
+
vectors: {
|
|
1117
|
+
byDimension,
|
|
1118
|
+
byModel: modelBreakdown,
|
|
1119
|
+
unsearchable,
|
|
1120
|
+
retrievable: currentDim ? (dimensionBreakdown.get(currentDim) ?? 0) : null,
|
|
1121
|
+
},
|
|
1067
1122
|
// 配了、库里有东西可嵌、却一条都没嵌上 = 真的降级(本机此前长期
|
|
1068
1123
|
// 处于 ready:true + embeddedCount:0,面板却显示正常)。
|
|
1069
1124
|
degraded: configured === true && total > 0 && embedded === 0,
|
package/lib/client.js
CHANGED
|
@@ -495,6 +495,11 @@ window.__ModuleLoader__.load({
|
|
|
495
495
|
"memory.features.tagDefault.hint": "这一项用的是出厂默认值,没有被你改过",
|
|
496
496
|
"memory.features.tagChanged": "已改",
|
|
497
497
|
"memory.features.tagChanged.hint": "你改过这一项,它不再用默认值",
|
|
498
|
+
// 「待重启」是第三态,不是「已改」的加强版:#481 之前面板只有「已改」,
|
|
499
|
+
// 用户改完总闸看到徽标亮起就以为生效了,实际运行时还在用启动时的旧值。
|
|
500
|
+
"memory.features.tagPending": "待重启",
|
|
501
|
+
"memory.features.tagPending.hint": "这一项已存进配置,但本次启动还没用上——重启 DSH(或在插件管理里关开一次本插件)后才生效",
|
|
502
|
+
"memory.features.pendingRestartNotice": "有改动还没生效:标着「待重启」的开关要等重启 DSH 后才起作用(在插件管理里把本插件关开一次也行)。",
|
|
498
503
|
"memory.features.autoSummarize": "自动把对话记下来(自动注入之外)",
|
|
499
504
|
"memory.features.autoSummarize.hint": "开:对话结束自动提炼成长期记忆条目(autoSummarize);关:只有你手动存或 Agent 主动存才有记忆",
|
|
500
505
|
"memory.features.hotMemoryEnabled": "顺带带上最近几轮原话",
|
|
@@ -676,6 +681,12 @@ window.__ModuleLoader__.load({
|
|
|
676
681
|
"memory.features.sleepProvider": "睡眠 Provider",
|
|
677
682
|
"memory.features.sleepModel": "睡眠模型",
|
|
678
683
|
"memory.features.sleepModelHint": "留空 = 用巩固模型或当前模型;建议选非思考模型",
|
|
684
|
+
// Issue #478:max_tokens 预算键。hint 写清两件事:这是「单轮输出预算」,
|
|
685
|
+
// 以及服务商硬上限更小时后端会按报错自动降级重试(对齐 #478 的后端行为)。
|
|
686
|
+
"memory.features.dreamMaxTokens": "巩固单轮输出上限 (max_tokens)",
|
|
687
|
+
"memory.features.dreamMaxTokens.hint": "单轮巩固最多生成多少 token,256–131072。默认 131072 超过部分服务商硬上限时,后端会按报错里的上限自动降级重试",
|
|
688
|
+
"memory.features.sleepMaxTokens": "睡眠单轮输出上限 (max_tokens)",
|
|
689
|
+
"memory.features.sleepMaxTokens.hint": "睡眠阶段冲突/模式判断的单轮输出预算,256–131072;超上限同样自动降级",
|
|
679
690
|
"memory.features.routeFollowDefault": "跟随默认路由",
|
|
680
691
|
"memory.features.modelTest": "测试连通性",
|
|
681
692
|
"memory.features.modelTesting": "测试中…",
|
|
@@ -1156,6 +1167,9 @@ window.__ModuleLoader__.load({
|
|
|
1156
1167
|
"memory.features.tagDefault.hint": "This one is at its factory default — you have not changed it",
|
|
1157
1168
|
"memory.features.tagChanged": "changed",
|
|
1158
1169
|
"memory.features.tagChanged.hint": "You changed this one; it no longer uses the default",
|
|
1170
|
+
"memory.features.tagPending": "restart pending",
|
|
1171
|
+
"memory.features.tagPending.hint": "Saved, but not yet in effect for this session — restart DSH (or toggle this plugin off and on in the plugin manager) to apply it",
|
|
1172
|
+
"memory.features.pendingRestartNotice": "Some changes are not in effect yet: switches marked \"restart pending\" apply only after you restart DSH (toggling this plugin off and on in the plugin manager works too).",
|
|
1159
1173
|
"memory.features.autoSummarize": "Turn conversations into memory automatically",
|
|
1160
1174
|
"memory.features.autoSummarize.hint": "On: memory entries are distilled when a conversation ends (autoSummarize). Off: memory only exists if you save it, or the agent decides to",
|
|
1161
1175
|
"memory.features.hotMemoryEnabled": "Carry the last few turns verbatim",
|
|
@@ -1329,6 +1343,12 @@ window.__ModuleLoader__.load({
|
|
|
1329
1343
|
"memory.features.sleepProvider": "Sleep provider",
|
|
1330
1344
|
"memory.features.sleepModel": "Sleep model",
|
|
1331
1345
|
"memory.features.sleepModelHint": "Leave empty to reuse the consolidation model; a non-reasoning model is recommended",
|
|
1346
|
+
// Issue #478: max_tokens budget keys. Hint states both the scope (single-run
|
|
1347
|
+
// output budget) and the backend auto-downgrade on provider cap rejection.
|
|
1348
|
+
"memory.features.dreamMaxTokens": "Consolidation output cap (max_tokens)",
|
|
1349
|
+
"memory.features.dreamMaxTokens.hint": "Max tokens generated per consolidation run, 256–131072. When the default 131072 exceeds a provider's hard limit, the backend retries with the cap from the error",
|
|
1350
|
+
"memory.features.sleepMaxTokens": "Sleep output cap (max_tokens)",
|
|
1351
|
+
"memory.features.sleepMaxTokens.hint": "Single-run output budget for sleep conflict/mode calls, 256–131072; over-cap values are downgraded automatically",
|
|
1332
1352
|
"memory.features.routeFollowDefault": "Follow default route",
|
|
1333
1353
|
"memory.features.modelTest": "Test connectivity",
|
|
1334
1354
|
"memory.features.modelTesting": "Testing…",
|
|
@@ -1830,6 +1850,13 @@ window.__ModuleLoader__.load({
|
|
|
1830
1850
|
".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}",
|
|
1831
1851
|
".mneme-set-row:hover .mneme-set-del,.mneme-set-row:focus-within .mneme-set-del{opacity:1}",
|
|
1832
1852
|
".mneme-set-del:hover{color:var(--mneme-danger);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1853
|
+
// 人格卡删除 × 独立类:不复用 mneme-set-del——其 opacity:0 仅在
|
|
1854
|
+
// .mneme-set-row:hover 解除,卡标签不在该容器里 → × 永远透明(v1.2
|
|
1855
|
+
// 实测踩中)。#457 曾用内联 opacity:1 覆写,但悬停变红仍走
|
|
1856
|
+
// .mneme-set-del:hover,连带常点亮了规则区的真删除按钮,被 #459 回滚。
|
|
1857
|
+
// 独立类常显,只沿用配色,规则区行为分毫不动。
|
|
1858
|
+
".mneme-persona-del{flex:none;opacity:1;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-size:16px;line-height:18px;padding:0 3px;border-radius:6px}",
|
|
1859
|
+
".mneme-persona-del:hover{color:var(--mneme-danger);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1833
1860
|
".mneme-set-cmd{flex:1;min-width:0}",
|
|
1834
1861
|
".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--mneme-accent)}",
|
|
1835
1862
|
".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px;word-break:break-word}",
|
|
@@ -1967,6 +1994,13 @@ window.__ModuleLoader__.load({
|
|
|
1967
1994
|
".mneme-conflict-primary:disabled{opacity:.55;cursor:default}",
|
|
1968
1995
|
".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--mneme-accent) 85%,var(--dsw-alias-label-primary))}",
|
|
1969
1996
|
".mneme-badge--frozen{color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 12%,transparent)}",
|
|
1997
|
+
// 「待重启」徽标:沿用 frozen 的 warn 语义(都是「现状不是你刚设的样子」),
|
|
1998
|
+
// 但加边框把它和只读冻结态区分开——它需要用户动手,不是终态。
|
|
1999
|
+
".mneme-badge--pending{color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 12%,transparent);border:1px solid color-mix(in srgb,var(--mneme-warn) 30%,transparent)}",
|
|
2000
|
+
".mneme-pending{display:flex;align-items:flex-start;gap:8px;margin:8px 0 0;padding:8px 10px;border-radius:8px;font-size:12.5px;line-height:18px;color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 10%,transparent);border:1px solid color-mix(in srgb,var(--mneme-warn) 28%,transparent)}",
|
|
2001
|
+
// 圆点用当前色(currentColor)而不是再引一次 --mneme-warn:色值只有一个真源,
|
|
2002
|
+
// 改主题时提示行与圆点不会走散。
|
|
2003
|
+
".mneme-pending-dot{flex:none;width:6px;height:6px;margin-top:6px;border-radius:50%;background:currentColor}",
|
|
1970
2004
|
".mneme-conflict-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:4px}",
|
|
1971
2005
|
".mneme-conflict-empty-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
1972
2006
|
".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
|
|
@@ -2910,7 +2944,7 @@ window.__ModuleLoader__.load({
|
|
|
2910
2944
|
// 后端白名单、面板够不着——而 CONFIGURATION.md 明写「面板可启停」。
|
|
2911
2945
|
// 其余整数键(429 调速器参数、distillMaxChars、codingBoostFactor)是有意
|
|
2912
2946
|
// 不上 UI 的调优噪音,见上方注释,别顺手搬进来。
|
|
2913
|
-
const FEATURE_NUMBERS = ["distillRecords.retentionDays"];
|
|
2947
|
+
const FEATURE_NUMBERS = ["distillRecords.retentionDays", "dreamMaxTokens", "sleepMaxTokens"];
|
|
2914
2948
|
const EMBED_PROVIDERS = ["openai", "local", "ollama"];
|
|
2915
2949
|
// 实体抽取思考强度(issue #109):与后端 FEATURE_FLAG_ENUMS 枚举对齐。
|
|
2916
2950
|
const ENTITY_REASONING = ["none", "low", "medium", "high"];
|
|
@@ -3063,11 +3097,25 @@ window.__ModuleLoader__.load({
|
|
|
3063
3097
|
// = 值仍来自 config schema 的默认值。面板自己判断,不加后端字段、
|
|
3064
3098
|
// 不动 config.js(#427 的验收红线之一)。
|
|
3065
3099
|
const ovr = (state && state.overrides) || {};
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3100
|
+
// 「待重启」是第三态,必须由后端算:#481 的坑正是「面板显示已改 ≠ 运行时
|
|
3101
|
+
// 已改」——开关在宿主启动时合并进 cfg 并冻结,此后改 kv 只是持久化。这个
|
|
3102
|
+
// 落差前端看不见(overrides 里只有「改了」这一个事实),所以后端另比一次
|
|
3103
|
+
// 冻结值并给出 pendingRestart 名单;本面板只负责把它显示出来。
|
|
3104
|
+
const pending = new Set((state && state.pendingRestart) || []);
|
|
3105
|
+
const flagTag = (k) => {
|
|
3106
|
+
if (pending.has(k)) {
|
|
3107
|
+
return {
|
|
3108
|
+
kind: "mneme-badge--pending",
|
|
3109
|
+
text: t("memory.features.tagPending"),
|
|
3110
|
+
title: t("memory.features.tagPending.hint")
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
return {
|
|
3114
|
+
kind: Object.prototype.hasOwnProperty.call(ovr, k) ? "mneme-badge--changed" : "mneme-badge--default",
|
|
3115
|
+
text: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged" : "memory.features.tagDefault"),
|
|
3116
|
+
title: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged.hint" : "memory.features.tagDefault.hint")
|
|
3117
|
+
};
|
|
3118
|
+
};
|
|
3071
3119
|
const boolRow = (k, opts = {}) => h(FeatureRow, {
|
|
3072
3120
|
key: k,
|
|
3073
3121
|
name: t(`memory.features.${k}`),
|
|
@@ -3256,14 +3304,19 @@ window.__ModuleLoader__.load({
|
|
|
3256
3304
|
// Issue #239 第 4 项镜像到巩固:高峰时段串(空 = 关闭)。放在 autoDream
|
|
3257
3305
|
// 子块内——它是巩固的排程,开关关掉时不该还在界面上留着可编辑的输入框。
|
|
3258
3306
|
strRow("dreamPeakHours"),
|
|
3259
|
-
h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamPeakHours.hint"))
|
|
3307
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamPeakHours.hint")),
|
|
3308
|
+
// Issue #478:单轮巩固的 max_tokens 预算。后端白名单(256–131072)早就有,
|
|
3309
|
+
// 修复前 UI 够不着;默认 131072 超过很多 OpenAI 兼容网关的硬上限又看不见调不了。
|
|
3310
|
+
numRow("dreamMaxTokens")
|
|
3260
3311
|
);
|
|
3261
3312
|
|
|
3262
3313
|
// 睡眠模型:sleepModeEnabled 开着才展开(sleepProvider/sleepModel 随本版
|
|
3263
3314
|
// 进白名单;下拉与测试复用同一 /llm-providers 数据源)。
|
|
3264
3315
|
const sleepSub = eff.sleepModeEnabled && Array.isArray(routes) && h("div", { className: "mneme-featsub" },
|
|
3265
3316
|
routeSelects("sleepProvider", "sleepModel", sleepTest, setSleepTest, "sleepReasoningEffort"),
|
|
3266
|
-
h("div", { className: "mneme-featsubhint" }, t("memory.features.sleepModelHint"))
|
|
3317
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.sleepModelHint")),
|
|
3318
|
+
// Issue #478:睡眠侧同款预算键(#257 引入后端白名单,UI 一直缺席)。
|
|
3319
|
+
numRow("sleepMaxTokens")
|
|
3267
3320
|
);
|
|
3268
3321
|
|
|
3269
3322
|
// 实体抽取路由与思考强度(issue #109):entityExtractionEnabled 开着才
|
|
@@ -3325,6 +3378,14 @@ window.__ModuleLoader__.load({
|
|
|
3325
3378
|
savedTick && h("span", { className: "mneme-saved", style: { marginLeft: 8, fontWeight: 400 } }, t("memory.features.restartHint"))
|
|
3326
3379
|
),
|
|
3327
3380
|
h("div", { className: "mneme-set-desc" }, t("memory.features.desc")),
|
|
3381
|
+
// #481:改了但没生效的开关,在这张卡里明说。放在 desc 之后、错误行之前
|
|
3382
|
+
// ——它是常态提示而非错误,但比错误更该先被看见(用户刚改完就会看这里)。
|
|
3383
|
+
// 行级 badge(flagTag 的第三态)标出具体是哪几行,这条只负责说清「有事
|
|
3384
|
+
// 没生效」和「怎么办」。
|
|
3385
|
+
pending.size > 0 && h("div", { className: "mneme-pending" },
|
|
3386
|
+
h("span", { className: "mneme-pending-dot" }),
|
|
3387
|
+
h("span", null, t("memory.features.pendingRestartNotice"))
|
|
3388
|
+
),
|
|
3328
3389
|
// 见 saveError 的声明处:这条分支不能跟着加载失败那条走。
|
|
3329
3390
|
saveError && h("div", { className: "mneme-mainterr" }, saveError),
|
|
3330
3391
|
h(RuntimeProvisionBlock, { t }),
|
|
@@ -3857,9 +3918,11 @@ window.__ModuleLoader__.load({
|
|
|
3857
3918
|
className: "mneme-btn" + (personaEditing === id ? " mneme-btn--active" : ""),
|
|
3858
3919
|
onClick: () => setPersonaEditing(id)
|
|
3859
3920
|
}, id),
|
|
3921
|
+
// 卡删除 × 常显:用独立类 mneme-persona-del(见上方样式定义处的
|
|
3922
|
+
// 注释——为何不复用 mneme-set-del,以及 #457→#459 一修一回滚的教训)。
|
|
3860
3923
|
h("button", {
|
|
3861
3924
|
type: "button",
|
|
3862
|
-
className: "mneme-
|
|
3925
|
+
className: "mneme-persona-del",
|
|
3863
3926
|
title: t("memory.settings.personaDeleteCard"),
|
|
3864
3927
|
"aria-label": t("memory.settings.personaDeleteCard") + " " + id,
|
|
3865
3928
|
onClick: () => removePersonaCard(id)
|
package/lib/config.js
CHANGED
|
@@ -863,6 +863,23 @@ export const Config = z.object({
|
|
|
863
863
|
// 数据、观察 A2 加权质量后再切硬过滤。
|
|
864
864
|
strictScope: z.boolean().default(false),
|
|
865
865
|
|
|
866
|
+
// scopeState(issue #471):政策轴——作用域级可逆闸门。与上面两个键是**两条不同
|
|
867
|
+
// 的轴**:scopeEnabled/strictScope 管「这条记忆属于谁」(事实轴:持久、跟着记忆
|
|
868
|
+
// 行走、改的是归属数据),本键管「这个作用域现在通不通」(政策轴:暂时、由人单
|
|
869
|
+
// 方面按下、松开即恢复、不改任何记忆行)。
|
|
870
|
+
// 两级语义同 writeAdmission/preInjectGate——enabled 打开时被按下的作用域其候选
|
|
871
|
+
// 照常注入,但留一条审计行说清「本应被拦下几条」(观察档,先在真实流量看会少掉
|
|
872
|
+
// 多少再决定真按);enforce 打开才真的不注入(拦截档)。两个都默认关,打开前
|
|
873
|
+
// 行为与今天逐字段一致。
|
|
874
|
+
// 不进 LIGHT_MODE_OFF:它只是查一张小表的判定,不引入重型后台路径、不加常驻
|
|
875
|
+
// 文本(同 actionPipelineEnabled 的定性)——轻量档预设该关的是重型能力,不该替
|
|
876
|
+
// 用户关掉一个纯判定入口,用户显式勾选仍然赢。
|
|
877
|
+
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
878
|
+
scopeState: z.object({
|
|
879
|
+
enabled: z.boolean().default(false),
|
|
880
|
+
enforce: z.boolean().default(false)
|
|
881
|
+
}).default({}),
|
|
882
|
+
|
|
866
883
|
// --- v0.9.0 M.O.(Modus Operandi · 手法,beta)--------------------------
|
|
867
884
|
// 命名:M.O. = Modus Operandi,拉丁语「一个人一贯的做事方式」,缩写 M.O.;
|
|
868
885
|
// 也正是 org 词根 modus(modusensus)。界面主显示 M.O.,设置项 hint 第一句
|
package/lib/dream/sleep.js
CHANGED
|
@@ -21,7 +21,7 @@ import { randomUUID, createHash } from "node:crypto";
|
|
|
21
21
|
import { validateDecisions, applyDecisions, ACTIONS } from "./decisions.js";
|
|
22
22
|
import { findPotentialConflicts, cosineSimilarity } from "./clustering.js";
|
|
23
23
|
import { scopeKeyOf } from "../scope.js";
|
|
24
|
-
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback, maintainIndexAfterDream } from "../dream.js";
|
|
24
|
+
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback, withMaxTokensFallback, maintainIndexAfterDream } from "../dream.js";
|
|
25
25
|
import { computeHeat } from "../heat.js";
|
|
26
26
|
|
|
27
27
|
// Issue #126 review(Copilot):默认档(sleepActionSet="conflict")允许的动作 = 全局
|
|
@@ -172,6 +172,22 @@ function makeSummary(m) {
|
|
|
172
172
|
return text.length <= SUMMARY_MAX ? text : `${text.slice(0, SUMMARY_MAX)}…`;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// issue #471 政策轴:被按下的作用域也不参与睡眠整理。与 dream.js 的 withoutPaused
|
|
176
|
+
// 同源(那里记着「判定只许有一份实现」的理由),但这里是**单独一处**而不是复用:
|
|
177
|
+
// 两个模块各被独立装配(runSleep / runDream 各自接 service),任何一个被换进只
|
|
178
|
+
// 实现了一半 service 的夹具时都该各自降级,而不是因为对方模块没装而双双失效。
|
|
179
|
+
//
|
|
180
|
+
// 睡眠侧比日间加固侧更要紧:这几个相位不只读——phaseDemotion 会 setArchived 与
|
|
181
|
+
// demoteToSummary(真改归档面),phasePatterns 会 mint 出 type=pattern 的新行
|
|
182
|
+
// (凭空给被按下的作用域生出衍生记忆)。只挡日间那几处读入口的话,闸门开着而每
|
|
183
|
+
// 6 小时一次的睡眠照样动手,那等于「按下」只是让它在白天看不见。
|
|
184
|
+
//
|
|
185
|
+
// service 上取不到 filterPaused 时(老装配、测试夹具)原样返回,与改动前一致。
|
|
186
|
+
function withoutPaused(service, rows) {
|
|
187
|
+
if (typeof service?.filterPaused !== "function") return rows;
|
|
188
|
+
return service.filterPaused(rows);
|
|
189
|
+
}
|
|
190
|
+
|
|
175
191
|
// ---------------------------------------------------------------- phases
|
|
176
192
|
|
|
177
193
|
/**
|
|
@@ -189,7 +205,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
189
205
|
}
|
|
190
206
|
const strictness = config.sleepConflictStrictness ?? "normal";
|
|
191
207
|
const threshold = CONFLICT_THRESHOLDS[strictness] ?? CONFLICT_THRESHOLDS.normal;
|
|
192
|
-
const memories = service.all().filter((m) => !m.archived && !m.forgotten && m.type !== "summary" && m.type !== "document");
|
|
208
|
+
const memories = withoutPaused(service, service.all().filter((m) => !m.archived && !m.forgotten && m.type !== "summary" && m.type !== "document"));
|
|
193
209
|
if (memories.length < 2) return { status: "skipped", reason: "too few memories" };
|
|
194
210
|
if (signal?.aborted) return { status: "aborted", reason: "user activity" };
|
|
195
211
|
|
|
@@ -313,7 +329,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
313
329
|
// actionSet 已在候选构造前解析(跨类型候选需要它),这里只按档位选 prompt:
|
|
314
330
|
// 默认档保持只有 conflict/keep 的窄 prompt,"full" 才开放六分支。
|
|
315
331
|
const conflictPrompt = actionSet === "full" ? STR.prompts.conflictFull[language] : STR.prompts.conflict[language];
|
|
316
|
-
const runConflict = (withEffort) => {
|
|
332
|
+
const runConflict = (withEffort, tokens) => {
|
|
317
333
|
conflictStreamFailure = "";
|
|
318
334
|
return auditedSleepLlm(ctx, service, config, logger, {
|
|
319
335
|
triggerSource: "sleep",
|
|
@@ -331,7 +347,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
331
347
|
provider: route.provider,
|
|
332
348
|
model: route.model,
|
|
333
349
|
purpose: "sleep-conflict",
|
|
334
|
-
maxTokens:
|
|
350
|
+
maxTokens: tokens,
|
|
335
351
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
336
352
|
messages: [
|
|
337
353
|
{ role: "system", content: [{ type: "text", text: conflictPrompt }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
@@ -339,7 +355,9 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
339
355
|
]
|
|
340
356
|
}, reportUsage, (reason) => { conflictStreamFailure = describeStreamFailure(reason); }));
|
|
341
357
|
};
|
|
342
|
-
|
|
358
|
+
// Issue #478:睡眠侧同样接 max_tokens 降级——面板 hint 已承诺「超上限同样自动降级」,
|
|
359
|
+
// 只挂 dream 三个点会让 sleepMaxTokens 的 131072 上限继续原样发。
|
|
360
|
+
const text = await withMaxTokensFallback(ctx, config.sleepMaxTokens ?? 2048, (tokens) => withEffortFallback(ctx, sleepEffort, () => runConflict(true, tokens), () => runConflict(false, tokens), () => conflictStreamFailure), () => conflictStreamFailure);
|
|
343
361
|
if (text === undefined) {
|
|
344
362
|
if (conflictStreamFailure) ctx.logger?.warn?.(`dsh-mneme sleep: conflict stream aborted or errored (${conflictStreamFailure})`);
|
|
345
363
|
return { status: "failed", error: "llm failed" };
|
|
@@ -423,7 +441,10 @@ function phaseDemotion(service, config, logger, runId, signal = null) {
|
|
|
423
441
|
const compressCut = Date.now() - compressDays * 86400000;
|
|
424
442
|
const demoted = [];
|
|
425
443
|
const archived = [];
|
|
426
|
-
|
|
444
|
+
// 政策轴在这里尤其要紧:下面两行会真改归档面(setArchived / demoteToSummary)。
|
|
445
|
+
// 把过滤放在循环之外、而不是在循环体里 continue:一旦将来有人在循环体里加
|
|
446
|
+
// 副作用(现已有),漏掉那条 continue 就是静默改到被按下的行。
|
|
447
|
+
for (const m of withoutPaused(service, service.all())) {
|
|
427
448
|
if (signal?.aborted) break;
|
|
428
449
|
if (m.archived || m.forgotten) continue;
|
|
429
450
|
const ref = m.last_accessed_at ?? m.created_at;
|
|
@@ -472,9 +493,11 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
472
493
|
const limit = config.sleepPatternMinMemories ?? 100;
|
|
473
494
|
// #230:document 在查询层排除(excludeTypes)而不是截断后过滤——LIMIT 200
|
|
474
495
|
// 先生效的话,指针行一多就会把普通记忆挤出扫描窗,池子饿到门槛以下。
|
|
475
|
-
|
|
496
|
+
// 政策轴同理必须排在 `.slice(0, limit)` **之前**,否则被按下的行照样占掉
|
|
497
|
+
// sleepPatternMinMemories 的名额,而它们既不会被扫、也不该挤掉别人(#471)。
|
|
498
|
+
const memories = withoutPaused(service, service
|
|
476
499
|
.list({ limit: 200, includeForgotten: false, excludeTypes: ["document"] })
|
|
477
|
-
.filter((m) => !m.archived && m.type !== "summary" && m.type !== "pattern")
|
|
500
|
+
.filter((m) => !m.archived && m.type !== "summary" && m.type !== "pattern"))
|
|
478
501
|
.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1))
|
|
479
502
|
.slice(0, limit);
|
|
480
503
|
if (memories.length === 0) return { status: "skipped", reason: "no memories to scan" };
|
|
@@ -485,7 +508,7 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
485
508
|
const maxPatterns = config.sleepMaxPatternPerRun ?? 3;
|
|
486
509
|
const sleepEffort = await resolveDreamEffort(ctx, route, config.sleepReasoningEffort, logger);
|
|
487
510
|
let patternStreamFailure = "";
|
|
488
|
-
const runPattern = (withEffort) => {
|
|
511
|
+
const runPattern = (withEffort, tokens) => {
|
|
489
512
|
patternStreamFailure = "";
|
|
490
513
|
return auditedSleepLlm(ctx, service, config, logger, {
|
|
491
514
|
triggerSource: "sleep",
|
|
@@ -500,7 +523,7 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
500
523
|
provider: route.provider,
|
|
501
524
|
model: route.model,
|
|
502
525
|
purpose: "sleep-pattern",
|
|
503
|
-
maxTokens:
|
|
526
|
+
maxTokens: tokens,
|
|
504
527
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
505
528
|
messages: [
|
|
506
529
|
{ role: "system", content: [{ type: "text", text: STR.prompts.pattern[language].replace("N", String(maxPatterns)) }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
@@ -508,7 +531,7 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
508
531
|
]
|
|
509
532
|
}, reportUsage, (reason) => { patternStreamFailure = describeStreamFailure(reason); }));
|
|
510
533
|
};
|
|
511
|
-
const text = await withEffortFallback(ctx, sleepEffort, () => runPattern(true), () => runPattern(false), () => patternStreamFailure);
|
|
534
|
+
const text = await withMaxTokensFallback(ctx, config.sleepMaxTokens ?? 2048, (tokens) => withEffortFallback(ctx, sleepEffort, () => runPattern(true, tokens), () => runPattern(false, tokens), () => patternStreamFailure), () => patternStreamFailure);
|
|
512
535
|
if (text === undefined) {
|
|
513
536
|
if (patternStreamFailure) ctx.logger?.warn?.(`dsh-mneme sleep: pattern stream aborted or errored (${patternStreamFailure})`);
|
|
514
537
|
return { status: "failed", error: "llm failed" };
|
|
@@ -563,7 +586,7 @@ function phaseRelations(service, config, logger, runId, signal = null) {
|
|
|
563
586
|
if (entities.length < 2) return { status: "skipped", reason: "too few entities" };
|
|
564
587
|
const orphans = entities.filter((e) => (service.getRelations(e.id) ?? []).length === 0);
|
|
565
588
|
if (orphans.length === 0) return { status: "skipped", reason: "no orphan entities" };
|
|
566
|
-
const memories = service.all().filter((m) => !m.archived && !m.forgotten);
|
|
589
|
+
const memories = withoutPaused(service, service.all().filter((m) => !m.archived && !m.forgotten));
|
|
567
590
|
const seen = new Set();
|
|
568
591
|
const related = [];
|
|
569
592
|
const MAX_RELATIONS_PER_ORPHAN = 3;
|