@modusensus/dsh-mneme 0.8.10 → 0.8.12
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 +5 -5
- package/assets/icon.svg +3 -0
- package/lib/client.js +190 -58
- package/lib/config.js +47 -1
- package/lib/dream/decisions.js +16 -0
- package/lib/dream/sleep.js +3 -0
- package/lib/dream.js +57 -9
- package/lib/entities/extractor.js +4 -1
- package/lib/graph/anchoring.js +180 -0
- package/lib/index.js +5 -0
- package/lib/inject.js +15 -3
- package/lib/lang.js +10 -0
- package/lib/service.js +164 -23
- package/lib/settings.js +24 -2
- package/lib/store.js +155 -11
- package/lib/summarize.js +104 -7
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +12 -7
- package/scripts/sync-test-badge.mjs +9 -7
- package/scripts/test-count-sync.mjs +44 -0
- package/src/config.js +47 -1
- package/src/dream/decisions.js +16 -0
- package/src/dream/sleep.js +3 -0
- package/src/dream.js +57 -9
- package/src/entities/extractor.js +4 -1
- package/src/graph/anchoring.js +180 -0
- package/src/index.js +5 -0
- package/src/inject.js +15 -3
- package/src/lang.js +10 -0
- package/src/service.js +164 -23
- package/src/settings.js +24 -2
- package/src/store.js +155 -11
- package/src/summarize.js +104 -7
- package/test/api.test.js +7 -2
- package/test/baseline-persist.test.js +248 -0
- package/test/client.test.js +245 -45
- package/test/dream.test.js +75 -0
- package/test/graph-anchoring.test.js +185 -0
- package/test/graph-inject-budget.test.js +104 -0
- package/test/graph-passive-confirm.test.js +92 -0
- package/test/graph-weight-evolution.test.js +105 -0
- package/test/scope-inject-soft.test.js +106 -0
- package/test/summarize.test.js +177 -0
- package/test/test-count-sync.test.js +60 -0
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)
|
|
@@ -253,7 +253,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
253
253
|
|
|
254
254
|
### 前置条件
|
|
255
255
|
|
|
256
|
-
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)— 兼容 DSH 0.1.x
|
|
256
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)— 兼容 DSH 0.1.x 与 0.2.x(三段式 peer 范围见 `package.json`);开发与 CI 已对齐 0.2 线,`0.2.0-rc.1` / `0.2.0-rc.2` 实测全绿。旧版兼容垫片(`Session.events` → `snapshotEvents()`,v0.7.9 起确保进入发布产物 lib/)保留
|
|
257
257
|
- Node 24+(`node:sqlite`)
|
|
258
258
|
|
|
259
259
|
### 安装步骤
|
|
@@ -430,7 +430,7 @@ dsh web
|
|
|
430
430
|
| `searchSemanticDedupThreshold` | `0.95` | 语义去重相似度阈值(v0.5.0,默认 0.95,范围 0.5-1.0):`searchSemanticDedup=true` 时生效,调整可防小模型误折叠 |
|
|
431
431
|
| `memoryQualityFilter` | `{enabled:true, archiveThreshold:30, degradeThreshold:60, minContentLength:10, exemptImportance:4}` | 记忆质量过滤(v0.4.6,默认开):写库前启发式打分 0-100,元记忆词汇/自指/过短/重复/近似重复扣分;≥60 正常存储,30-60 降权(注入排序按 importance×quality/100),<30 归档标记 `low_quality`(显式搜索仍可召回,永不自动注入;`exemptImportance` 豁免:importance ≥ 该值只降权不归档,#135) |
|
|
432
432
|
| `llmAudit` | `{enabled:true, retentionDays:90}` | LLM 消耗审计(v0.4.6,默认开):每次后台 LLM 调用(`autoDream` / `autoSummarize` / `sleep` / `entityExtract`,对应 `operation_type` 为 `dream_consolidate` / `dream_summarize` / `dream_narrative`、`summarize_compress`、`sleep_conflict` / `sleep_pattern`、`entity_extract`)写 `llm_audit_logs`(tokens/duration/status/source);失败记 error 不阻塞;只读 API `/api/dsh-mneme/semantic/llm-audit` + `/llm-audit/stats` |
|
|
433
|
-
| `scopeEnabled` | `false` | 作用域隔离总开关(v0.8.0,issue #17):memory_save 按会话身份写入 agent / workspace 标注(agentPreset / 工作区路径,registry 反查取不到回退 header.cwd,再取不到 NULL
|
|
433
|
+
| `scopeEnabled` | `false` | 作用域隔离总开关(v0.8.0,issue #17):memory_save 按会话身份写入 agent / workspace 标注(agentPreset / 工作区路径,registry 反查取不到回退 header.cwd,再取不到 NULL);去重键扩展含作用域三元——跨作用域同标题不再物理合并;检索与注入排序按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见,issue #339 补齐注入通道)。也走 feature_flags 白名单(面板可启停) |
|
|
434
434
|
| `strictScope` | `false` | 作用域硬过滤(v0.8.0 引入,v0.8.1 起只认显式声明;依赖 `scopeEnabled`):检索 / 注入 / 列表 / 单取四路过滤,**显式声明**收窄到他者作用域的记忆完全不可见;载体自动标注只降权保留可见——真正的物理隔离请用 sensitivity。会话身份解析不到时 fail-closed 只挡显式行。关闭时全部为软隔离(降权保留可见)。也走 feature_flags 白名单 |
|
|
435
435
|
| `writeAdmission` | `{enabled:false, enforce:false}` | 写入准入(issue #254,默认关):把「这条该不该进库」前移到 LLM 之前。第 1 级是零 LLM 的确定性判据——空白 / 纯噪声 + 密钥 / PII(判据来源是 #164 A2,经 `sensitiveScan` 注入,本批只定义接口)。`enabled` 跑判据并落审计行(`llm_audit_logs` 的 `metadata.deny`),`enforce` 才真的拒(`store.save` 之前返回,`memory_save` 返回 `action:"denied"` + `reason`);只开 `enabled` = 仅告警、写入不拦。去重键命中不进第 1 级(归 write-update 放行),G1/G2 阈值只计量。也走 feature_flags 白名单 |
|
|
436
436
|
| `conflictFreezeEnabled` | `false` | 冲突冻结(v0.4.4):dream 发现矛盾对不自动裁决,冻结进冲突队列;状态页「冲突队列」支持并排对比与人工确认(保留 A / 保留 B / 仅标记已处理,v0.8.0) |
|
|
@@ -584,7 +584,7 @@ src/
|
|
|
584
584
|
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
|
|
585
585
|
└── index.js # 插件接线
|
|
586
586
|
lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
|
|
587
|
-
test/ #
|
|
587
|
+
test/ # 1497 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
|
|
588
588
|
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 运行时清单
|
|
589
589
|
```
|
|
590
590
|
|
|
@@ -593,7 +593,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
593
593
|
```bash
|
|
594
594
|
cd dsh-mneme
|
|
595
595
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
596
|
-
npm test # 运行
|
|
596
|
+
npm test # 运行 1497 个测试
|
|
597
597
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
598
598
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
599
599
|
```
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 16 16" fill="none" stroke="#4176e6" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M4.43 12.31C2.19 10.17 2.27 6.81 4.18 4.84C6.10 2.88 9.06 2.99 10.74 4.68C12.43 6.37 12.28 8.92 10.82 10.33C9.35 11.74 7.22 11.55 6.08 10.32C4.95 9.08 5.18 7.35 6.18 6.50C6.57 6.17 7.03 5.99 7.48 5.97"/>
|
|
3
|
+
</svg>
|
package/lib/client.js
CHANGED
|
@@ -68,19 +68,37 @@ window.__ModuleLoader__.load({
|
|
|
68
68
|
if (ops.filter((o) => o.kind !== "same").length > 200) return null;
|
|
69
69
|
return ops;
|
|
70
70
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
71
|
+
// --- mneme mark:插件自己的标识图标 -------------------------------------
|
|
72
|
+
// 不再借宿主的 IconArchiveOutline*,两个原因:
|
|
73
|
+
// · 语义撞车——「归档盒」和记忆库里的归档动作是同一个符号,用户反馈看不出
|
|
74
|
+
// 这是记忆插件(issue 走维护者自开通道);
|
|
75
|
+
// · 宿主耦合——宿主图标名会换代(#287:Outline20 → OutlineRegular/Medium),
|
|
76
|
+
// 只认某一代就取到 undefined,h(undefined) 即 React #130 把整个 slot entry
|
|
77
|
+
// 崩掉。自绘后这一整类问题消失,也不再需要「探测多代名字 + 缺名降级」。
|
|
78
|
+
// 形状:单线螺旋(记忆痕迹 / 召回回路)。规格逐条对齐宿主图标,才不像外来户:
|
|
79
|
+
// 16 栅格、fill:none、stroke:currentColor、描边 1(宿主 Regular 档)、圆头圆角。
|
|
80
|
+
// 路径是 6 段三次贝塞尔拟合同一螺旋(拟合版与逐点采样版逐像素一致,已核验),
|
|
81
|
+
// 所以源码里只有 200 字符,不是一百多个采样点。
|
|
82
|
+
// 改形状只需换 MNEME_MARK_D,并同步 assets/icon.svg(插件列表那份,同一个 d)。
|
|
83
|
+
const MNEME_MARK_D = "M4.43 12.31C2.19 10.17 2.27 6.81 4.18 4.84C6.10 2.88 9.06 2.99 10.74 4.68C12.43 6.37 12.28 8.92 10.82 10.33C9.35 11.74 7.22 11.55 6.08 10.32C4.95 9.08 5.18 7.35 6.18 6.50C6.57 6.17 7.03 5.99 7.48 5.97";
|
|
84
|
+
|
|
85
|
+
/** mneme mark 的内联 SVG;currentColor + 描边 1,随主题与字号走。 */
|
|
86
|
+
const MnemeMark = ({ size = 16, className }) => h("svg", {
|
|
87
|
+
width: size,
|
|
88
|
+
height: size,
|
|
89
|
+
className,
|
|
90
|
+
viewBox: "0 0 16 16",
|
|
91
|
+
fill: "none",
|
|
92
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
93
|
+
"aria-hidden": "true",
|
|
94
|
+
stroke: "currentColor",
|
|
95
|
+
strokeWidth: 1,
|
|
96
|
+
strokeLinecap: "round",
|
|
97
|
+
strokeLinejoin: "round"
|
|
98
|
+
}, h("path", { d: MNEME_MARK_D }));
|
|
99
|
+
|
|
100
|
+
/** 渲染 mneme mark:四个入口(浮层标题栏 / footer 回退 / 侧边栏 portal / better-sidebar tab)共用同一颗。 */
|
|
101
|
+
const renderMnemeMark = (props) => h(MnemeMark, props);
|
|
84
102
|
|
|
85
103
|
// Portal target for the hero fallback surface. The host whitelists
|
|
86
104
|
// react-dom for its own bundles (dsh-client-ui-trajectory requires it);
|
|
@@ -1328,11 +1346,26 @@ window.__ModuleLoader__.load({
|
|
|
1328
1346
|
".mneme-heat--warm{color:var(--dsw-alias-label-secondary)}",
|
|
1329
1347
|
".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}",
|
|
1330
1348
|
".mneme-heat--cold .mneme-heatpct{display:none}",
|
|
1331
|
-
//
|
|
1349
|
+
// 侧边栏入口两种形态的样式(挂载与搬移逻辑见 SidebarTopEntry 的头注释)。
|
|
1350
|
+
// 展开态兜底:逐条镜像宿主 panelRow(36px 行高 / 透明底 / 2px 外边距 /
|
|
1351
|
+
// 7px 8px 内边距)——宿主面板行类名在或不在,观感都是「插件 / 自动化任务」
|
|
1352
|
+
// 那一档,不会再退回 v0.8.10 那种高亮大按钮。
|
|
1353
|
+
// 收起态:按钮带宿主 newSession 类名,标题栏固定几何(position:fixed、
|
|
1354
|
+
// 顶部居中、28px 圆钮、次级文字色、hover 填充)整套继承宿主规则,我们只用
|
|
1355
|
+
// 一条更具体的规则把 left 从宿主的 48px 推到 84px——即「新会话」右侧,
|
|
1356
|
+
// 不再像 v0.8.10 那样压在它身上(用户实测确认的遮挡 bug)。
|
|
1357
|
+
// 84 = 48(宿主给「新会话」的 left)+ 28(钮宽)+ 8(间距);120 = 84 + 28 + 8,
|
|
1358
|
+
// 让「应用 / 编辑」菜单顺延到我们右侧。宿主侧出处:SidebarRoot.module.css 的
|
|
1359
|
+
// `.collapsed .newSession{left:48px}` 与
|
|
1360
|
+
// `html[data-windows-titlebar]:has([data-sidebar-collapsed=true]){--dsh-windows-menu-start:84px}`。
|
|
1332
1361
|
".mneme-topentry{display:flex;flex-direction:column;position:relative}",
|
|
1333
|
-
".mneme-topentry-native{
|
|
1362
|
+
".mneme-topentry-native{box-sizing:border-box;min-height:36px;border:none;border-radius:var(--dsw-radius-md);background:0 0;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;align-items:center;gap:8px;margin:0 2px;padding:7px 8px;line-height:22px;display:flex}",
|
|
1334
1363
|
".mneme-topentry-native:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1364
|
+
".mneme-topentry-native:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}",
|
|
1335
1365
|
".mneme-topentry-native .mneme-topentry-label{white-space:nowrap}",
|
|
1366
|
+
"html[data-windows-titlebar] .mneme-topentry .mneme-topentry-rail{left:84px}",
|
|
1367
|
+
// 菜单让位只在「收起 + 入口确实挂在标题栏里」时生效;展开态完全不碰宿主变量。
|
|
1368
|
+
"html[data-windows-titlebar]:has([data-sidebar-collapsed=true]):has([data-plugin-entry=\"@modusensus/dsh-mneme\"]){--dsh-windows-menu-start:120px}",
|
|
1336
1369
|
// --- 功能开关:一行一开关,Claude 式安静排版 ---
|
|
1337
1370
|
".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);margin:16px 0 2px}",
|
|
1338
1371
|
".mneme-featrow{display:flex;align-items:center;gap:14px;padding:11px 2px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
|
|
@@ -3031,6 +3064,10 @@ window.__ModuleLoader__.load({
|
|
|
3031
3064
|
// MemoryExplorer 内部状态(选中行、过滤)由各自的跳转回调处理。
|
|
3032
3065
|
function openLibrary() {
|
|
3033
3066
|
setOverlayOpen(true);
|
|
3067
|
+
// 打开面板 = 用户正准备处理冻结冲突:顺手把入口红点重取一次,别让他
|
|
3068
|
+
// 对着一个已经过期的计数(裁决走别的路径、或后台 autoDream 刚写过库时,
|
|
3069
|
+
// 计数不该等 60s 兜底轮询才对齐)。
|
|
3070
|
+
conflictBadgeCount.refresh();
|
|
3034
3071
|
}
|
|
3035
3072
|
|
|
3036
3073
|
// --- Hero fallback overlay state (module-level pub/sub) ---
|
|
@@ -3098,7 +3135,7 @@ window.__ModuleLoader__.load({
|
|
|
3098
3135
|
h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label"), ref: panelRef },
|
|
3099
3136
|
h("div", { className: "mneme-overlaybar" },
|
|
3100
3137
|
h("span", { className: "mneme-overlaytitle" },
|
|
3101
|
-
|
|
3138
|
+
renderMnemeMark({ size: 15 }),
|
|
3102
3139
|
t("memory.view.label")
|
|
3103
3140
|
),
|
|
3104
3141
|
h("button", {
|
|
@@ -4924,32 +4961,80 @@ window.__ModuleLoader__.load({
|
|
|
4924
4961
|
);
|
|
4925
4962
|
}
|
|
4926
4963
|
|
|
4927
|
-
// --- Sidebar entry:
|
|
4964
|
+
// --- Sidebar entry: 侧边栏里的记忆入口 ---
|
|
4928
4965
|
// 注册仍在 sidebar.footer.action(list 插槽)——它既是 React 树的挂载
|
|
4929
|
-
// 锚点,也是 portal
|
|
4930
|
-
//
|
|
4931
|
-
//
|
|
4932
|
-
//
|
|
4933
|
-
//
|
|
4934
|
-
//
|
|
4935
|
-
//
|
|
4966
|
+
// 锚点,也是 portal 失效时的原位回退;常驻 sheet 也从这个插槽挂载。
|
|
4967
|
+
// 入口形状跟着宿主 v0.2 的侧边栏走,分两态:
|
|
4968
|
+
// · 展开态:宿主把「插件 / 自动化任务」这类全局面板渲染成 nav.panelList
|
|
4969
|
+
// 里的 panelRow(36px 行高、透明底、图标 + 标题)。我们把按钮 portal 进
|
|
4970
|
+
// 同一组并原样套用宿主 panelRow 的实时类名,于是它是该组的第三行——低调,
|
|
4971
|
+
// 而不是 v0.8.10 那样长得像第二个高亮「新会话」按钮。
|
|
4972
|
+
// · 收起态(Windows 标题栏):panelList / regionArea / footArea 被宿主
|
|
4973
|
+
// display:none,只有开关与「新会话」被改成固定在标题栏上。我们把按钮搬回
|
|
4974
|
+
// 标题栏、并排在「新会话」右侧(84px / 120px 的算法见上面 CSS 注释),
|
|
4975
|
+
// 三颗图标各就各位——v0.8.10 把入口留在宿主的 48px 上把它盖住了。
|
|
4976
|
+
// 承载类名一律从活元素读取(宿主/皮肤会异步改写哈希类名),不硬编码;
|
|
4977
|
+
// 面板列表不存在(没装任何面板插件)时退回「新会话」行之后。
|
|
4936
4978
|
// 宿主结构异常时约 2 秒后放弃 portal,footer 回退按钮保持可用。
|
|
4937
|
-
// #177:未处理冲突数 badge——数据走 dream-status 的 pendingConflicts
|
|
4938
|
-
//
|
|
4939
|
-
//
|
|
4940
|
-
|
|
4941
|
-
|
|
4979
|
+
// #177:未处理冲突数 badge——数据走 dream-status 的 pendingConflicts(与状态卡
|
|
4980
|
+
// 同一端点)。挂侧边栏入口按钮右上角(portal 与回退两处都挂),让用户不进面板
|
|
4981
|
+
// 也知道有冻结要裁决。
|
|
4982
|
+
// 刷新时机(用户反馈「裁决完红点消失得很慢」后重做):
|
|
4983
|
+
// · 裁决成功时冲突队列广播 mneme:conflicts-changed → 立刻重取,不必等下一轮;
|
|
4984
|
+
// 此前这条广播只有状态卡订阅,红点漏订,才慢到要等 60s 兜底轮询;
|
|
4985
|
+
// · openLibrary 打开面板时重取(见上);
|
|
4986
|
+
// · 60s 兜底轮询——后台 autoDream 自己产生/消解冲突时没人广播,靠它浮现。
|
|
4987
|
+
// 计数是模块级单例:入口按钮的 portal 与 footer 回退是两个组件,原先各起一个
|
|
4988
|
+
// 60s 轮询(两份请求、两个互不同步的计数)。现在共享一个 store:一次取回广播给
|
|
4989
|
+
// 全部订阅者,in-flight 请求也去重,最后一个订阅者离开才停表。
|
|
4990
|
+
const conflictBadgeCount = (() => {
|
|
4991
|
+
const listeners = new Set();
|
|
4992
|
+
let value = 0;
|
|
4993
|
+
let timer = null;
|
|
4994
|
+
let inflight = null;
|
|
4995
|
+
const publish = (next) => {
|
|
4996
|
+
if (next === value) return;
|
|
4997
|
+
value = next;
|
|
4998
|
+
listeners.forEach((fn) => {
|
|
4999
|
+
try { fn(value); } catch { /* 单个订阅者出错不拖垮其余 */ }
|
|
5000
|
+
});
|
|
5001
|
+
};
|
|
5002
|
+
const refresh = () => {
|
|
5003
|
+
if (inflight) return inflight;
|
|
5004
|
+
inflight = apiFetch("/api/dsh-mneme/dream-status")
|
|
5005
|
+
.then((res) => (res.ok ? res.json() : { pendingConflicts: 0 }))
|
|
5006
|
+
.then((d) => { publish(Number((d && d.pendingConflicts) ?? 0) || 0); })
|
|
5007
|
+
.catch(() => { publish(0); })
|
|
5008
|
+
.finally(() => { inflight = null; });
|
|
5009
|
+
return inflight;
|
|
5010
|
+
};
|
|
5011
|
+
return {
|
|
5012
|
+
get: () => value,
|
|
5013
|
+
refresh,
|
|
5014
|
+
subscribe: (fn) => {
|
|
5015
|
+
listeners.add(fn);
|
|
5016
|
+
fn(value);
|
|
5017
|
+
if (listeners.size === 1) {
|
|
5018
|
+
refresh();
|
|
5019
|
+
timer = setInterval(refresh, 60_000);
|
|
5020
|
+
}
|
|
5021
|
+
return () => {
|
|
5022
|
+
listeners.delete(fn);
|
|
5023
|
+
if (listeners.size === 0 && timer !== null) { clearInterval(timer); timer = null; }
|
|
5024
|
+
};
|
|
5025
|
+
}
|
|
5026
|
+
};
|
|
5027
|
+
})();
|
|
5028
|
+
function useConflictBadgeCount() {
|
|
5029
|
+
const [pending, setPending] = useState(() => conflictBadgeCount.get());
|
|
4942
5030
|
useEffect(() => {
|
|
4943
|
-
|
|
4944
|
-
const
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
5031
|
+
const unsubscribe = conflictBadgeCount.subscribe(setPending);
|
|
5032
|
+
const onChanged = () => { conflictBadgeCount.refresh(); };
|
|
5033
|
+
window.addEventListener("mneme:conflicts-changed", onChanged);
|
|
5034
|
+
return () => {
|
|
5035
|
+
unsubscribe();
|
|
5036
|
+
window.removeEventListener("mneme:conflicts-changed", onChanged);
|
|
4949
5037
|
};
|
|
4950
|
-
tick();
|
|
4951
|
-
const timer = setInterval(tick, 60_000);
|
|
4952
|
-
return () => { cancelled = true; clearInterval(timer); };
|
|
4953
5038
|
}, []);
|
|
4954
5039
|
return pending;
|
|
4955
5040
|
}
|
|
@@ -4959,7 +5044,7 @@ window.__ModuleLoader__.load({
|
|
|
4959
5044
|
pending > 99 ? "99+" : String(pending));
|
|
4960
5045
|
}
|
|
4961
5046
|
function SidebarFallbackTrigger({ wide, t }) {
|
|
4962
|
-
const pending = useConflictBadgeCount(
|
|
5047
|
+
const pending = useConflictBadgeCount();
|
|
4963
5048
|
// #295 评审:.mneme-trigger 自身 overflow:hidden 且无定位上下文,badge
|
|
4964
5049
|
// 直接放里面会被裁剪——包一层定位容器,badge 挂在容器上。
|
|
4965
5050
|
return h("div", { className: "mneme-triggerwrap" },
|
|
@@ -4973,7 +5058,7 @@ window.__ModuleLoader__.load({
|
|
|
4973
5058
|
onClick: openLibrary,
|
|
4974
5059
|
"data-mneme-overlay-opener": "true"
|
|
4975
5060
|
},
|
|
4976
|
-
|
|
5061
|
+
renderMnemeMark({ size: wide ? 16 : 18 }),
|
|
4977
5062
|
wide && h("span", { className: "mneme-trigger-label" }, t("memory.panel.open"))
|
|
4978
5063
|
),
|
|
4979
5064
|
h(ConflictBadge, { pending })
|
|
@@ -4982,14 +5067,25 @@ window.__ModuleLoader__.load({
|
|
|
4982
5067
|
|
|
4983
5068
|
function SidebarTopEntry({ wide, t, fallback }) {
|
|
4984
5069
|
const [host, setHost] = useState(null);
|
|
4985
|
-
const [
|
|
4986
|
-
const pending = useConflictBadgeCount(
|
|
5070
|
+
const [cls, setCls] = useState({ row: "", glyph: "", title: "" });
|
|
5071
|
+
const pending = useConflictBadgeCount();
|
|
4987
5072
|
useEffect(() => {
|
|
4988
5073
|
if (!reactDom || typeof document === "undefined") return undefined;
|
|
4989
5074
|
let tries = 0, timer = null, created = null, mo = null;
|
|
5075
|
+
const findRegion = () => {
|
|
5076
|
+
const region = document.querySelector('[class*="regionArea"]');
|
|
5077
|
+
return region && region.parentElement ? region : null;
|
|
5078
|
+
};
|
|
4990
5079
|
const findNative = (region) =>
|
|
4991
5080
|
region.parentElement.querySelector('[class*="newSession"]')
|
|
4992
5081
|
|| region.previousElementSibling;
|
|
5082
|
+
const findPanelList = (region) =>
|
|
5083
|
+
region.parentElement.querySelector('[class*="panelList"]');
|
|
5084
|
+
// 宿主自己的面板行:排除我们自己 portal 进去的那颗按钮(它也带 panelRow)。
|
|
5085
|
+
const findPanelRow = (region) => {
|
|
5086
|
+
const list = findPanelList(region);
|
|
5087
|
+
return list ? list.querySelector('[class*="panelRow"]:not(.mneme-topentry-native)') : null;
|
|
5088
|
+
};
|
|
4993
5089
|
// 占位锚点:新会话行(现役外壳里 newSession 按钮嵌在 logoRow 内,
|
|
4994
5090
|
// 旧外壳是根的直接子按钮)。判定与生态插件的 sidebar-entry-core
|
|
4995
5091
|
// 一致;锚点不可靠时回退到 regionArea 之前的旧位置。
|
|
@@ -5008,32 +5104,61 @@ window.__ModuleLoader__.load({
|
|
|
5008
5104
|
// insertBefore(x, x) 在 Chromium 里不是 no-op,会触发 mutation 造成
|
|
5009
5105
|
// 观察器自激风暴、冻死整个 SPA。
|
|
5010
5106
|
const place = (region) => {
|
|
5107
|
+
if (!created) return;
|
|
5011
5108
|
const parent = region.parentElement;
|
|
5012
5109
|
if (!parent) return;
|
|
5110
|
+
// 展开态落进宿主的面板列表(紧跟「插件 / 自动化任务」);宿主没有面板
|
|
5111
|
+
// 列表时退回「新会话」行之后,由 .mneme-topentry-native 兜住低调行样式。
|
|
5112
|
+
const list = wide ? findPanelList(region) : null;
|
|
5113
|
+
if (list) {
|
|
5114
|
+
if (created.parentElement === list && created === list.lastElementChild) return;
|
|
5115
|
+
list.appendChild(created);
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
5013
5118
|
const anchor = findAnchor(region);
|
|
5014
5119
|
const target = anchor ? anchor.nextSibling : region;
|
|
5015
5120
|
if (target === created) return;
|
|
5016
5121
|
if (created.parentElement === parent && created.nextSibling === target) return;
|
|
5017
5122
|
parent.insertBefore(created, target);
|
|
5018
5123
|
};
|
|
5124
|
+
// 类名一律从活元素读。展开态借宿主面板行的盒模型,并剔掉激活态修饰类
|
|
5125
|
+
// (否则我们的行会跟着宿主当前选中的面板一起常亮);收起态借「新会话」
|
|
5126
|
+
// 按钮的类名拿整套标题栏固定几何。两处都取不到时留空,由我们的类兜底。
|
|
5127
|
+
const readCls = (region) => {
|
|
5128
|
+
if (!wide) return { row: findNative(region)?.className || "", glyph: "", title: "" };
|
|
5129
|
+
const row = findPanelRow(region);
|
|
5130
|
+
const btn = row || findNative(region);
|
|
5131
|
+
const glyph = row && row.querySelector('[class*="panelGlyph"]');
|
|
5132
|
+
const title = row && row.querySelector('[class*="panelTitle"]');
|
|
5133
|
+
return {
|
|
5134
|
+
row: (btn?.className || "")
|
|
5135
|
+
.split(/\s+/)
|
|
5136
|
+
.filter((name) => name && !name.includes("panelActive"))
|
|
5137
|
+
.join(" "),
|
|
5138
|
+
glyph: (glyph && glyph.className) || "",
|
|
5139
|
+
title: (title && title.className) || ""
|
|
5140
|
+
};
|
|
5141
|
+
};
|
|
5142
|
+
const sync = (region) => {
|
|
5143
|
+
const next = readCls(region);
|
|
5144
|
+
setCls((prev) => (prev.row === next.row && prev.glyph === next.glyph && prev.title === next.title ? prev : next));
|
|
5145
|
+
};
|
|
5019
5146
|
const attempt = () => {
|
|
5020
|
-
const region =
|
|
5021
|
-
if (region
|
|
5022
|
-
//
|
|
5023
|
-
//
|
|
5024
|
-
//
|
|
5025
|
-
// 否则 portal 按钮会停留在捕获时刻的旧类上(宽度/对齐失配)。
|
|
5147
|
+
const region = findRegion();
|
|
5148
|
+
if (region) {
|
|
5149
|
+
// 宿主切换展开/收起时会重排 DOM(面板列表 → 标题栏固定位),所以每次
|
|
5150
|
+
// 变动都重新落位并重读类名;宿主/皮肤异步改写类名也靠这条同步,否则
|
|
5151
|
+
// portal 按钮会停留在捕获时刻的旧类上(宽度/对齐失配)。
|
|
5026
5152
|
created = document.createElement("div");
|
|
5027
5153
|
created.dataset.pluginEntry = "@modusensus/dsh-mneme";
|
|
5028
5154
|
place(region);
|
|
5029
5155
|
setHost(created);
|
|
5030
|
-
|
|
5156
|
+
sync(region);
|
|
5031
5157
|
mo = new MutationObserver(() => {
|
|
5032
|
-
const cur =
|
|
5033
|
-
if (!cur
|
|
5158
|
+
const cur = findRegion();
|
|
5159
|
+
if (!cur) return;
|
|
5034
5160
|
place(cur);
|
|
5035
|
-
|
|
5036
|
-
setNativeCls((prev) => (prev === cls ? prev : cls));
|
|
5161
|
+
sync(cur);
|
|
5037
5162
|
});
|
|
5038
5163
|
mo.observe(region.parentElement, {
|
|
5039
5164
|
attributes: true,
|
|
@@ -5052,13 +5177,19 @@ window.__ModuleLoader__.load({
|
|
|
5052
5177
|
if (mo) mo.disconnect();
|
|
5053
5178
|
if (created && created.parentElement) created.parentElement.removeChild(created);
|
|
5054
5179
|
};
|
|
5055
|
-
}, []);
|
|
5180
|
+
}, [wide]);
|
|
5056
5181
|
if (!host) return fallback;
|
|
5182
|
+
// 图标与「插件 / 自动化任务」同档(16px);收起态沿用标题栏里原来的 18px。
|
|
5183
|
+
const icon = renderMnemeMark({ size: wide ? 16 : 18 });
|
|
5057
5184
|
return reactDom.createPortal(
|
|
5058
5185
|
h("div", { className: "mneme-topentry" },
|
|
5059
5186
|
h("button", {
|
|
5060
5187
|
type: "button",
|
|
5061
|
-
|
|
5188
|
+
// 展开态:宿主面板行类名 + 我们的同款兜底;收起态:宿主「新会话」类名
|
|
5189
|
+
// + 只覆盖 left 的 rail 规则(整套标题栏几何继承宿主)。
|
|
5190
|
+
className: wide
|
|
5191
|
+
? `${cls.row} mneme-topentry-native`.trim()
|
|
5192
|
+
: `${cls.row} mneme-topentry-rail`.trim(),
|
|
5062
5193
|
"aria-label": pending > 0
|
|
5063
5194
|
? `${t("memory.sidebar.aria")} · ${t("memory.status.conflicts")} ${pending}`
|
|
5064
5195
|
: t("memory.sidebar.aria"),
|
|
@@ -5066,8 +5197,9 @@ window.__ModuleLoader__.load({
|
|
|
5066
5197
|
onClick: openLibrary,
|
|
5067
5198
|
"data-mneme-overlay-opener": "true"
|
|
5068
5199
|
},
|
|
5069
|
-
|
|
5070
|
-
|
|
5200
|
+
// 图标套上宿主面板行的槽位类名,视觉与同组入口完全一致;取不到时裸渲染。
|
|
5201
|
+
cls.glyph ? h("span", { className: cls.glyph, "aria-hidden": "true" }, icon) : icon,
|
|
5202
|
+
wide && h("span", { className: `${cls.title} mneme-topentry-label`.trim() }, t("memory.panel.open")),
|
|
5071
5203
|
h(ConflictBadge, { pending })
|
|
5072
5204
|
)
|
|
5073
5205
|
), host);
|
|
@@ -5128,7 +5260,7 @@ window.__ModuleLoader__.load({
|
|
|
5128
5260
|
reg.registerTab({
|
|
5129
5261
|
id: TAB_ID,
|
|
5130
5262
|
title: () => t("memory.view.label"),
|
|
5131
|
-
icon: (size) =>
|
|
5263
|
+
icon: (size) => renderMnemeMark({ size }),
|
|
5132
5264
|
order: 60,
|
|
5133
5265
|
component: () => h(MemoryExplorer, { t })
|
|
5134
5266
|
});
|
package/lib/config.js
CHANGED
|
@@ -281,6 +281,14 @@ export const Config = z.object({
|
|
|
281
281
|
// 显式开启后放宽跨类型合并检查(类型边界由用户自行承担);配合
|
|
282
282
|
// dreamSkipInvalid 理解:关闭 skipInvalid 时跨类型 merge 直接整单拒绝。
|
|
283
283
|
allowCrossTypeMerge: z.boolean().default(false),
|
|
284
|
+
// issue #339 / E8 考卷:merge 护栏(opt-in,默认关 = 现状)。开启后合并对象
|
|
285
|
+
// 命中长保留类型(preference/pattern/rejected_solution/constraint/pitfall,
|
|
286
|
+
// 与 archive 护栏同一张表)的 merge 决策整条跳过——E8 实测巩固损耗里
|
|
287
|
+
// 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉,而 archive 护栏只挡
|
|
288
|
+
// archive 不挡 merge,「更精炼的摘要」恰是约束失真的主通道。skipInvalid
|
|
289
|
+
// (默认开)时被跳条目进 dream_runs.skipped、run 记 degraded;关闭
|
|
290
|
+
// skipInvalid 时整单拒绝。与 allowCrossTypeMerge 同用时长 guard 先判。
|
|
291
|
+
dreamMergeGuard: z.boolean().default(false),
|
|
284
292
|
// Rule version for dream adjudication: when this bumps, older dream_runs
|
|
285
293
|
// degrade to historical evidence (their receipts no longer drive live
|
|
286
294
|
// decisions). Default 0 = no versioning in use yet.
|
|
@@ -463,6 +471,35 @@ export const Config = z.object({
|
|
|
463
471
|
// 检索融合池(与 BM25 同级的确认/回填信号)。默认关=检索行为与 #219 前
|
|
464
472
|
// 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
|
|
465
473
|
entityRecallEnabled: z.boolean().default(false),
|
|
474
|
+
// 边权重演化(issue #24 块2):touch 门控是否把被寻回路径的边权重抬升。
|
|
475
|
+
// 默认关=边初值纯由建边来源决定(store.RELATION_SOURCE_DEFAULTS),演化
|
|
476
|
+
// 不生效;开=被激活的边经 bumpRelationWeight 上涨(只加不减、封顶 1.0)。
|
|
477
|
+
graphWeightEnabled: z.boolean().default(false),
|
|
478
|
+
// 单次有效触达的抬升幅度。
|
|
479
|
+
graphWeightDelta: z.number().min(0).max(1).default(0.1),
|
|
480
|
+
// 被动确认(issue #24 块4):heat 触达(记忆被召回/注入=被用)即对该记忆
|
|
481
|
+
// 挂联的关系边做被动确认——bumpRelationWeight 抬一格。默认关=零行为变化;
|
|
482
|
+
// 开=正常触达演化边权,「关系是用出来的」闭环落地(异常路径复核留后续块)。
|
|
483
|
+
graphPassiveConfirm: z.boolean().default(false),
|
|
484
|
+
|
|
485
|
+
// --- 图谱锚定层(issue #24 · 块1 Activation Anchoring)-------------------
|
|
486
|
+
// entityRecallEnabled 之上再开一层:以查询命中的实体为种子,沿实体关系表
|
|
487
|
+
// 级联扩散(默认深度 2),把「邻居实体挂联的记忆」也带进融合池。种子集
|
|
488
|
+
// 上限 graphSeedCap、级联深度 graphCascadeDepth 一并下发到 anchorSeeds /
|
|
489
|
+
// cascadeDepths(见 src/graph/anchoring.js)。默认关=行为与 #219 单跳轴
|
|
490
|
+
// 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
|
|
491
|
+
graphAnchoringEnabled: z.boolean().default(false),
|
|
492
|
+
graphSeedCap: z.natural().min(1).max(30).default(12),
|
|
493
|
+
graphCascadeDepth: z.natural().min(1).max(3).default(2),
|
|
494
|
+
|
|
495
|
+
// 关联提示防幻觉(issue #24 块3):图召回的候选(查询命中实体的挂联记忆)
|
|
496
|
+
// 进注入时默认只参与排序、不改变注入块构成(graphInjectHint=false 保守档);
|
|
497
|
+
// 开启后注入文本里以固定句式「[检索线索] …」标出,引导语说明这是链路信息
|
|
498
|
+
// 非事实断言,防 LLM 单次消费把桥接召回当有依据的事实回头扩散。
|
|
499
|
+
graphInjectHint: z.boolean().default(false),
|
|
500
|
+
// 图召回候选在注入块里的独立预算(条数上限)。与 pin/document 预算同构:
|
|
501
|
+
// 不占 maxInjectedItems 名额、只约束「桥接线索」这类低信任信息的常驻占比。
|
|
502
|
+
graphInjectBudget: z.natural().min(0).max(5).default(1),
|
|
466
503
|
|
|
467
504
|
// --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
|
|
468
505
|
// Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
|
|
@@ -685,7 +722,8 @@ export const Config = z.object({
|
|
|
685
722
|
// agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
|
|
686
723
|
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
687
724
|
scopeEnabled: z.boolean().default(false),
|
|
688
|
-
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope
|
|
725
|
+
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见,
|
|
726
|
+
// 检索与注入排序同权重,issue #339 补齐注入通道);
|
|
689
727
|
// 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
|
|
690
728
|
// 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
|
|
691
729
|
// 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
|
|
@@ -712,6 +750,14 @@ const LIGHT_MODE_OFF = [
|
|
|
712
750
|
"bm25SearchEnabled",
|
|
713
751
|
// 轻量模式不开图召回轴(#219,依赖实体抽取产出;抽取本身已被关掉)。
|
|
714
752
|
"entityRecallEnabled",
|
|
753
|
+
// 轻量模式不开图谱锚定层级联(#24 块1,依赖实体抽取 + 关系表,抽取已关则无边可扩)。
|
|
754
|
+
"graphAnchoringEnabled",
|
|
755
|
+
// 轻量模式不开边权重演化(#24 块2,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
|
|
756
|
+
"graphWeightEnabled",
|
|
757
|
+
// 轻量模式不开关联提示标注(#24 块3,常驻注入文本多一段提示成本)。
|
|
758
|
+
"graphInjectHint",
|
|
759
|
+
// 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
|
|
760
|
+
"graphPassiveConfirm",
|
|
715
761
|
// 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
|
|
716
762
|
"dreamNarrativeEnabled",
|
|
717
763
|
// 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
|
package/lib/dream/decisions.js
CHANGED
|
@@ -210,6 +210,22 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
210
210
|
if (mergeTypes.size > 1 && options.allowCrossTypeMerge !== true) {
|
|
211
211
|
local.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
212
212
|
}
|
|
213
|
+
// issue #339 / E8 考卷:merge 护栏(opt-in,dreamMergeGuard)。巩固损耗
|
|
214
|
+
// 实测里 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉——archive
|
|
215
|
+
// 护栏(上方)只挡 archive 不挡 merge,而 merge 的「更精炼摘要」恰是
|
|
216
|
+
// 约束失真的主通道。开启后被合并对象命中 ARCHIVE_GUARDED_TYPES 的
|
|
217
|
+
// merge 决策整条跳过(与 archive 护栏同款通道:skipInvalid 时 skipped
|
|
218
|
+
// 明细、严格模式整单拒绝)。
|
|
219
|
+
if (local.length === 0 && options.mergeGuard === true) {
|
|
220
|
+
const guardedTypes = new Set();
|
|
221
|
+
for (const id of d.ids) {
|
|
222
|
+
const mem = snapshot.get(id);
|
|
223
|
+
if (mem && ARCHIVE_GUARDED_TYPES.has(mem.type)) guardedTypes.add(mem.type);
|
|
224
|
+
}
|
|
225
|
+
if (guardedTypes.size > 0) {
|
|
226
|
+
local.push(`${at}: merge of long-retention type(s) ${[...guardedTypes].join(", ")} is blocked by dreamMergeGuard`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
213
229
|
}
|
|
214
230
|
}
|
|
215
231
|
if (local.length > 0) {
|
package/lib/dream/sleep.js
CHANGED
|
@@ -373,6 +373,9 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
373
373
|
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
374
374
|
skipInvalid: config.dreamSkipInvalid !== false,
|
|
375
375
|
allowCrossTypeMerge: config.allowCrossTypeMerge === true,
|
|
376
|
+
// issue #339 / E8:guarded 类型 merge 护栏(opt-in,dreamMergeGuard),
|
|
377
|
+
// 与 dream 主链路同一判据。
|
|
378
|
+
mergeGuard: config.dreamMergeGuard === true,
|
|
376
379
|
// Issue #126 review(Copilot):默认档必须真的只放行旧动作集——只换 prompt 挡不住
|
|
377
380
|
// 模型自发输出 supersede / differentiate,那样"opt-in 零行为变化"就不成立。
|
|
378
381
|
allowedActions: actionSet === "full" ? null : LEGACY_ACTIONS
|