@modusensus/dsh-mneme 0.8.0 → 0.8.1
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 +4 -2
- package/lib/api.js +40 -12
- package/lib/client.js +130 -8
- package/lib/dream/sleep.js +43 -12
- package/lib/dream.js +43 -2
- package/lib/lang.js +11 -0
- package/lib/scope.js +39 -4
- package/lib/service.js +105 -18
- package/lib/store.js +158 -15
- package/lib/tools.js +58 -11
- package/lib/version-check.js +122 -0
- package/package.json +1 -1
- package/src/api.js +40 -12
- package/src/dream/sleep.js +43 -12
- package/src/dream.js +43 -2
- package/src/lang.js +11 -0
- package/src/scope.js +39 -4
- package/src/service.js +105 -18
- package/src/store.js +158 -15
- package/src/tools.js +58 -11
- package/src/version-check.js +122 -0
- package/test/conflict-freeze.test.js +15 -5
- package/test/scope-candidates.test.js +239 -0
- package/test/scope-explicit.test.js +452 -0
- package/test/scope-storage.test.js +13 -5
- package/test/scope-strict.test.js +107 -41
- package/test/version-check.test.js +152 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
| ① 写入 | ② 存储 | ③ 进化 |
|
|
23
23
|
|--------|--------|--------|
|
|
24
|
-
| 对话中模型主动记录(`memory_save`,可带敏感度与事件时间);会话结束自动提炼(`autoSummarize`) | SQLite 主库 + 人类可编辑 Markdown 镜像;实体 / 属性 / 时间轴三层结构化;记忆带 agent / workspace / 敏感度 / 发生时间标注 | 新会话自动注入相关记忆(按当前作用域加权,strictScope
|
|
24
|
+
| 对话中模型主动记录(`memory_save`,可带敏感度与事件时间);会话结束自动提炼(`autoSummarize`) | SQLite 主库 + 人类可编辑 Markdown 镜像;实体 / 属性 / 时间轴三层结构化;记忆带 agent / workspace / 敏感度 / 发生时间标注 | 新会话自动注入相关记忆(按当前作用域加权,strictScope 开启后显式收窄的归属硬隔离);autoDream 后台去重 / 合并 / 归档,冲突冻结待人工裁决 |
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
# 30 秒上手
|
|
@@ -228,6 +228,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
228
228
|
|
|
229
229
|
| 版本 | 亮点 |
|
|
230
230
|
|------|------|
|
|
231
|
+
| **v0.8.1** | 归属显式声明与人工纠偏 + 插件版本自检:记忆归属三段式可控——per-dim 来源列 + save/update scope 参数 + 面板可编辑(#170 第 1 步,#171);sleep 跨 scope 相似对停车进冲突队列人工裁决(第 2 步,#172);`strictScope` 硬过滤只认显式声明、自动标注降为纯软加权(第 3 步,#173,4.3 已确认);`GET /api/dsh-mneme/version-check` 只读路由 + 设置页偏差横幅(仅 outdated 渲染,registry 精确 host 白名单 + TTL 缓存 + 全失败静默,pnpm 钉子警示 + 市场收录延迟说明,#174 后续);1026 测试全绿 |
|
|
231
232
|
| **v0.8.0** | 作用域隔离(issue #17 批次 A)+ 冲突集中处理 + 斜杠命令提交 Agent + 注入转义回归修复:记忆按 agent 与工作区双维标注(agent_scope / workspace_scope / sensitivity / occurred_at 四列可空),检索按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见),opt-in `strictScope` 硬过滤贯通检索/注入/列表/单取(fail-closed);`scopeEnabled` / `strictScope` 默认关、面板「作用域隔离」组可启停;去重键扩展含作用域三元——跨作用域同标题不再物理合并;`occurred_at` 事件发生时间 + `occurred_from/to` 过滤贯通搜索与列表;冲突冻结(conflictFreezeEnabled)新增人工出口——状态页冲突队列并排对比 + 保留 A/B/仅标记(#166);斜杠命令经 agent.followup 真正提交模型(#152);恢复 v0.7.4 的注入花括号转义 + hot memory 跳过 reasoning——`{{.Server.Version}}` 类文本不再卡死会话(#165);988 测试全绿 |
|
|
232
233
|
| **v0.7.32** | 冲突动作集扩展 + 运行时自管化 + 记忆/提示语言 + 候选集 hybrid 档 + 一批修复:`sleepActionSet` 新增 `full` 档(supersede/differentiate/update/merge/conflict/keep 六分支,默认 `conflict` 零行为变化)——supersede 赢家正文干净、输家归档附「已被取代」注记,differentiate 双留+差异注记;sleep 校验接入 `dreamSkipInvalid` 宽容策略(默认 true,一票否决变宽容,严格可关);运行时三档取件(收编/本地 .tgz/registry)+ transformers 转可选 peer(#131);`memory.language`(zh/en)提示/注入/镜像语言可选(#124);summarize 节流 + 产出上限 + 同会话去重(#127);inject 正文读取修复(#129)、#135 五连修(前缀解析/effort 最低档/importance 豁免/ready 语义/覆盖度降级告警)、boot 回填指纹短路修复(#128)、degraded 轮跳过明细落库(#104);916 测试全绿、总覆盖 92.7%(runtime 96%、embedding 100%) |
|
|
233
234
|
| **v0.7.30** | 状态页向量卡修复(issue #118):改读开放的 `/semantic`——不再吃 `/vector-config` 的 401「加载失败」,ollama/local 模式不再恒显「未启用」;新增「初始化中(embedder 不可达,正在重试)」与「已索引 N / M 条」回填进度显示;legacy OpenAI 兼容 embedder「Object · 0D」显示修复(embedder 显式 `name: "OpenAI"` + 维度回退 `index.dimension`);「已索引 N / M」分子分母同口径(`embeddedCount` 剔除归档/遗忘,与 `count()` 默认过滤对齐);embedder init 失败有界重试(共 5 次,不再一次性永久降级,unload 清理定时器)+ `OllamaEmbedder.ready` 生命周期位 + `/semantic` 新增 `ready` 字段;745 测试全绿 |
|
|
@@ -335,6 +336,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
335
336
|
| **v0.7.31** | ✅ 完成 | peerDependencies 宿主版本声明修复 | 原 `^0.1.0-rc.6` 按 node-semver 预发布元组规则不匹配 `0.1.5-rc.1` 等中间预发布版本(当前 dsh 用户安装 ERESOLVE),也违反 awesome-dsh-plugin 的 peer-range 预发布分支规范;改显式三段式预发布分支(覆盖 0.1.0-rc.6 至 0.1.5-rc.1 全部已发布 0.1.x 含预发布 + 0.2 预发布留显式分支 + 0.3+ 待验证后放开);745 测试全绿 |
|
|
336
337
|
| **v0.7.32** | ✅ 完成 | sleep 冲突动作集扩展 + 运行时自管化 + 候选集 hybrid 档 + 一批修复 | `sleepActionSet` 新增 `full` 档(supersede/differentiate/update/merge/conflict/keep 六分支,默认 `conflict` 零行为变化)+ sleep 校验接入 `dreamSkipInvalid` 宽容策略 + 被跳决策带 phase 落库 `dream_runs.skipped`;运行时三档取件 + transformers 转可选 peer(#131);`memory.language` 提示/注入/镜像语言可选(#124);候选集向量驱动 hybrid 档(#125,PR #147);summarize 节流 + 产出上限 + 同会话去重(#127);inject 正文读取修复(#129)+ #135 五连修 + boot 回填指纹短路修复(#128)+ degraded 轮跳过明细落库(#104);runtime 覆盖统计归一 + 补齐(总覆盖 92.7%);916 测试全绿 |
|
|
337
338
|
| **v0.8.0** | ✅ 完成 | 作用域隔离 + 冲突队列 | 按 agent 与工作区双维隔离记忆(标注 / 检索加权 / opt-in strictScope 四路硬过滤,issue #17 批次 A);冻结冲突新增人工确认队列(状态页并排对比,#166);斜杠命令真正提交 Agent(#152);注入转义回归恢复 + hot memory 跳过 reasoning(#162)。图谱增强(兴趣漂移可视化)、跨 workspace 记忆共享仍在规划 |
|
|
339
|
+
| **v0.8.1** | ✅ 完成 | 归属显式声明与人工纠偏 + 插件版本自检 | 归属三段式可控:per-dim 来源列 + save/update scope 参数 + 面板可编辑(#170 第 1 步,#171);sleep 跨 scope 相似对停车人工裁决接冲突队列(第 2 步,#172);`strictScope` 硬过滤只认显式声明、自动标注降纯软加权(第 3 步,#173)。插件自报新版本:/version-check 只读路由 + 设置页 outdated 横幅(registry 精确白名单 + TTL 缓存 + 全失败静默,pnpm 钉子警示,#174 后续,#176);1026 测试全绿 |
|
|
338
340
|
|
|
339
341
|
> 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
|
|
340
342
|
|
|
@@ -461,7 +463,7 @@ dsh web
|
|
|
461
463
|
| `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) |
|
|
462
464
|
| `llmAudit` | `{enabled:true, retentionDays:90}` | LLM 消耗审计(v0.4.6,默认开):每次后台 LLM 调用(autoDream/autoSummarize)写 `llm_audit_logs`(tokens/duration/status/source);失败记 error 不阻塞;只读 API `/api/dsh-mneme/semantic/llm-audit` + `/llm-audit/stats` |
|
|
463
465
|
| `scopeEnabled` | `false` | 作用域隔离总开关(v0.8.0,issue #17):memory_save 按会话身份写入 agent / workspace 标注(agentPreset / 工作区路径,registry 反查取不到回退 header.cwd,再取不到 NULL);去重键扩展含作用域三元——跨作用域同标题不再物理合并;检索按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见)。也走 feature_flags 白名单(面板可启停) |
|
|
464
|
-
| `strictScope` | `false` | 作用域硬过滤(v0.8.0
|
|
466
|
+
| `strictScope` | `false` | 作用域硬过滤(v0.8.0 引入,v0.8.1 起只认显式声明;依赖 `scopeEnabled`):检索 / 注入 / 列表 / 单取四路过滤,**显式声明**收窄到他者作用域的记忆完全不可见;载体自动标注只降权保留可见——真正的物理隔离请用 sensitivity。会话身份解析不到时 fail-closed 只挡显式行。关闭时全部为软隔离(降权保留可见)。也走 feature_flags 白名单 |
|
|
465
467
|
| `conflictFreezeEnabled` | `false` | 冲突冻结(v0.4.4):dream 发现矛盾对不自动裁决,冻结进冲突队列;状态页「冲突队列」支持并排对比与人工确认(保留 A / 保留 B / 仅标记已处理,v0.8.0) |
|
|
466
468
|
| `escapePromptVariables` | `true` | 注入文本花括号转义(v0.8.0 恢复,issue #162):注入边界把 `{{...}}` 转义,防止 hot memory / 记忆原文里的 Go template / Vue 语法触发宿主 interpolate 抛错卡死会话(v0.7.4 曾修复、v0.7.11 误删) |
|
|
467
469
|
|
package/lib/api.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import { timingSafeEqual, randomBytes } from "node:crypto";
|
|
4
3
|
import { FEATURE_FLAG_SPEC } from "./settings.js";
|
|
5
4
|
import { TYPE_FILE, renderMirrorText, parseHumanEdits } from "./mirror.js";
|
|
@@ -8,6 +7,7 @@ import { computeHeat } from "./heat.js";
|
|
|
8
7
|
import { describeStreamFailure, resolveRoute } from "./dream.js";
|
|
9
8
|
import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
|
|
10
9
|
import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
|
|
10
|
+
import { classify, fetchLatestVersion, PACKAGE_VERSION } from "./version-check.js";
|
|
11
11
|
|
|
12
12
|
// headers:少数端点(/export 附件下载)需要追加 Content-Disposition 等响应头。
|
|
13
13
|
function sendJson(res, status, payload, headers = {}) {
|
|
@@ -24,16 +24,8 @@ function sendAttachment(res, status, contentType, filename, body) {
|
|
|
24
24
|
res.end(body);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// "unknown",导出本身仍然可用。
|
|
30
|
-
const PACKAGE_VERSION = (() => {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version ?? "unknown";
|
|
33
|
-
} catch {
|
|
34
|
-
return "unknown";
|
|
35
|
-
}
|
|
36
|
-
})();
|
|
27
|
+
// 版本号统一从 version-check.js 取(同一份读 package.json 逻辑,读取失败
|
|
28
|
+
// 同样降级 "unknown")——/info 与 /version-check 两个路由共用,不再各写一份。
|
|
37
29
|
|
|
38
30
|
// Defaults for the standalone external API — keep in step with the schema
|
|
39
31
|
// defaults in config.js (externalApiPort / externalApiHost).
|
|
@@ -175,6 +167,29 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
175
167
|
}
|
|
176
168
|
});
|
|
177
169
|
|
|
170
|
+
// 版本自检(#174 后续):运行版本 vs npm registry latest。只读,宿主围栏
|
|
171
|
+
// 统一鉴权(与 dream-status 同款,面板 apiFetch 直接可用)。registry 查询
|
|
172
|
+
// 全失败时 latest=null、status=unknown——前端对 unknown 完全静默,版本提示
|
|
173
|
+
// 是锦上添花,绝不因它新增故障面。
|
|
174
|
+
register({
|
|
175
|
+
kind: "exact",
|
|
176
|
+
path: "/api/dsh-mneme/version-check",
|
|
177
|
+
handler(req, res) {
|
|
178
|
+
if (req.method !== "GET") {
|
|
179
|
+
sendJson(res, 404, { error: "not-found" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
fetchLatestVersion()
|
|
183
|
+
.then((latest) => sendJson(res, 200, {
|
|
184
|
+
version: PACKAGE_VERSION,
|
|
185
|
+
latest,
|
|
186
|
+
status: classify(PACKAGE_VERSION, latest),
|
|
187
|
+
checkedAt: new Date().toISOString()
|
|
188
|
+
}))
|
|
189
|
+
.catch(() => sendJson(res, 200, { version: PACKAGE_VERSION, latest: null, status: "unknown" }));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
178
193
|
register({
|
|
179
194
|
kind: "exact",
|
|
180
195
|
path: "/api/dsh-mneme/list",
|
|
@@ -480,6 +495,19 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
480
495
|
}
|
|
481
496
|
patch.tags = body.tags;
|
|
482
497
|
}
|
|
498
|
+
// v0.8.1 底座(issue #170):显式 scope 修正。null=放宽到全局,
|
|
499
|
+
// 字符串=收窄/改标;归一化("global"/"*"/空白→NULL + explicit 章 +
|
|
500
|
+
// 审计行)在 service.updateMemory 内做,路由只做形状校验。
|
|
501
|
+
if (body.agent_scope !== undefined && body.agent_scope !== null && typeof body.agent_scope !== "string") {
|
|
502
|
+
sendJson(res, 400, { error: "invalid-agent-scope" });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (body.workspace_scope !== undefined && body.workspace_scope !== null && typeof body.workspace_scope !== "string") {
|
|
506
|
+
sendJson(res, 400, { error: "invalid-workspace-scope" });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (body.agent_scope !== undefined) patch.agent_scope = body.agent_scope;
|
|
510
|
+
if (body.workspace_scope !== undefined) patch.workspace_scope = body.workspace_scope;
|
|
483
511
|
if (body.archived !== undefined && typeof body.archived !== "boolean") {
|
|
484
512
|
sendJson(res, 400, { error: "invalid-archived" });
|
|
485
513
|
return;
|
|
@@ -501,7 +529,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
501
529
|
...history
|
|
502
530
|
].slice(0, 20);
|
|
503
531
|
}
|
|
504
|
-
if (Object.keys(patch).length) service.update(id, patch);
|
|
532
|
+
if (Object.keys(patch).length) service.update(id, patch, { actor: "panel" });
|
|
505
533
|
if (body.archived !== undefined) service.setArchived(id, body.archived);
|
|
506
534
|
sendJson(res, 200, { memory: service.getById(id) });
|
|
507
535
|
});
|
package/lib/client.js
CHANGED
|
@@ -270,6 +270,8 @@ window.__ModuleLoader__.load({
|
|
|
270
270
|
"memory.settings.feedback.email": "邮件反馈(work@modusensus.space)",
|
|
271
271
|
"memory.settings.feedback.browse": "浏览仓库已知问题",
|
|
272
272
|
"memory.settings.feedback.hint": "反馈前先搜搜是否已有相同问题,能省一份重复的 issue~",
|
|
273
|
+
"memory.settings.version.outdated": "🆕 有新版本 {v}(当前运行 {c})",
|
|
274
|
+
"memory.settings.version.outdatedHint": "若安装时指定过版本号,常规升级不会跨大/小版本(pnpm 钉子)——请重新安装或在升级命令加 --latest;npm 已发布而市场暂未收录属正常(约 1 天延迟)。",
|
|
273
275
|
"memory.settings.mode.title": "运行模式",
|
|
274
276
|
"memory.settings.mode.desc": "轻量模式只保留核心的记忆读写与自动注入(关闭 autoDream 巩固、实体抽取、语义搜索等高级功能),适合只想「记住偏好」的轻量使用;标准模式开启全部功能。",
|
|
275
277
|
"memory.settings.mode.light": "轻量",
|
|
@@ -351,7 +353,7 @@ window.__ModuleLoader__.load({
|
|
|
351
353
|
"memory.features.scopeEnabled": "作用域标注",
|
|
352
354
|
"memory.features.scopeEnabled.hint": "按会话身份(agent/工作区)给新记忆打归属标注;重启 DSH 后生效",
|
|
353
355
|
"memory.features.strictScope": "严格隔离",
|
|
354
|
-
"memory.features.strictScope.hint": "
|
|
356
|
+
"memory.features.strictScope.hint": "检索与注入硬过滤,只对显式声明的归属生效:显式收窄到他者作用域的记忆不可见;载体自动标注只降权保留可见(真正的物理隔离请用 sensitivity)。重启 DSH 后生效",
|
|
355
357
|
"memory.features.trustEpistemicWeighting": "可信度加权",
|
|
356
358
|
"memory.features.trustEpistemicWeighting.hint": "按来源可信度调整召回排序(实验性)",
|
|
357
359
|
"memory.features.reflectionFailureTracking": "反思失败追踪",
|
|
@@ -407,6 +409,12 @@ window.__ModuleLoader__.load({
|
|
|
407
409
|
"memory.explorer.scopeBadge": "作用域",
|
|
408
410
|
"memory.explorer.detail.agentScope": "Agent 作用域",
|
|
409
411
|
"memory.explorer.detail.workspaceScope": "工作区作用域",
|
|
412
|
+
"memory.explorer.detail.scopeGlobal": "全局(所有会话可见)",
|
|
413
|
+
"memory.explorer.detail.scopeSourceAuto": "自动",
|
|
414
|
+
"memory.explorer.detail.scopeSourceExplicit": "显式",
|
|
415
|
+
"memory.explorer.detail.scopeEditHint": "留空 = 全局;也可填写具体标签收窄归属",
|
|
416
|
+
"memory.explorer.detail.scopeWidenConfirm": "确认放宽可见性?",
|
|
417
|
+
"memory.explorer.detail.scopeWidenHint": "放宽后该记忆将对所有会话可见",
|
|
410
418
|
"memory.explorer.detail.sensitivity": "敏感度",
|
|
411
419
|
"memory.explorer.detail.occurred": "发生时间",
|
|
412
420
|
"memory.explorer.detail.edit": "编辑",
|
|
@@ -611,6 +619,8 @@ window.__ModuleLoader__.load({
|
|
|
611
619
|
"memory.settings.feedback.email": "Email feedback (work@modusensus.space)",
|
|
612
620
|
"memory.settings.feedback.browse": "Browse known issues",
|
|
613
621
|
"memory.settings.feedback.hint": "Search for an existing issue first — it saves a duplicate.",
|
|
622
|
+
"memory.settings.version.outdated": "🆕 New version {v} available (running {c})",
|
|
623
|
+
"memory.settings.version.outdatedHint": "If the plugin was installed with a pinned version, regular upgrades never cross minor/major lines — reinstall or pass --latest. A fresh npm release may take about a day to appear in the market.",
|
|
614
624
|
"memory.settings.mode.title": "Runtime mode",
|
|
615
625
|
"memory.settings.mode.desc": "Light mode keeps only the core memory read/write and auto-injection (autoDream consolidation, entity extraction and semantic search are off) — for light use where you just want preferences remembered. Standard mode enables everything.",
|
|
616
626
|
"memory.settings.mode.light": "Light",
|
|
@@ -692,7 +702,7 @@ window.__ModuleLoader__.load({
|
|
|
692
702
|
"memory.features.scopeEnabled": "Scope tagging",
|
|
693
703
|
"memory.features.scopeEnabled.hint": "Stamp new memories with the writing session's identity (agent/workspace); takes effect after restarting DSH",
|
|
694
704
|
"memory.features.strictScope": "Strict isolation",
|
|
695
|
-
"memory.features.strictScope.hint": "Hard-filter search and injection: memories
|
|
705
|
+
"memory.features.strictScope.hint": "Hard-filter search and injection for EXPLICITLY declared scopes only: memories explicitly narrowed to other agents/workspaces become invisible; auto carrier labels are demoted but stay visible (use sensitivity for true physical isolation). Takes effect after restarting DSH",
|
|
696
706
|
"memory.features.trustEpistemicWeighting": "Credibility weighting",
|
|
697
707
|
"memory.features.trustEpistemicWeighting.hint": "Adjust recall ranking by source credibility (experimental)",
|
|
698
708
|
"memory.features.reflectionFailureTracking": "Reflection failure tracking",
|
|
@@ -748,6 +758,12 @@ window.__ModuleLoader__.load({
|
|
|
748
758
|
"memory.explorer.scopeBadge": "Scoped",
|
|
749
759
|
"memory.explorer.detail.agentScope": "Agent scope",
|
|
750
760
|
"memory.explorer.detail.workspaceScope": "Workspace scope",
|
|
761
|
+
"memory.explorer.detail.scopeGlobal": "Global (visible to all sessions)",
|
|
762
|
+
"memory.explorer.detail.scopeSourceAuto": "auto",
|
|
763
|
+
"memory.explorer.detail.scopeSourceExplicit": "explicit",
|
|
764
|
+
"memory.explorer.detail.scopeEditHint": "Empty = global; or enter a specific label to narrow the scope",
|
|
765
|
+
"memory.explorer.detail.scopeWidenConfirm": "Widen visibility?",
|
|
766
|
+
"memory.explorer.detail.scopeWidenHint": "Widening makes this memory visible to all sessions",
|
|
751
767
|
"memory.explorer.detail.sensitivity": "Sensitivity",
|
|
752
768
|
"memory.explorer.detail.occurred": "Occurred",
|
|
753
769
|
"memory.explorer.detail.edit": "Edit",
|
|
@@ -2068,6 +2084,9 @@ window.__ModuleLoader__.load({
|
|
|
2068
2084
|
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
2069
2085
|
// 反馈入口的插件版本(GET /info)。失败保持 "unknown",链接仍可用。
|
|
2070
2086
|
const [pkgVersion, setPkgVersion] = react.useState("unknown");
|
|
2087
|
+
// 版本自检(#174 后续):运行版本 vs registry latest。仅 outdated 渲染
|
|
2088
|
+
// 横幅,其余状态(up-to-date / ahead / unknown / 查询失败)零渲染零打扰。
|
|
2089
|
+
const [updateInfo, setUpdateInfo] = react.useState(null);
|
|
2071
2090
|
// 运行模式 — light vs standard; null = still loading. The card keeps
|
|
2072
2091
|
// its own busy/saved/error state so it never blocks the others.
|
|
2073
2092
|
const [mode, setMode] = react.useState(null);
|
|
@@ -2113,6 +2132,21 @@ window.__ModuleLoader__.load({
|
|
|
2113
2132
|
return () => { cancelled = true; };
|
|
2114
2133
|
}, []);
|
|
2115
2134
|
|
|
2135
|
+
// 版本自检:只读 /version-check(宿主围栏鉴权)。非 outdated 一律归
|
|
2136
|
+
// null——横幅不出现即是「无需处理」;接口失败也静默,不新增故障面。
|
|
2137
|
+
react.useEffect(() => {
|
|
2138
|
+
let cancelled = false;
|
|
2139
|
+
apiFetch("/api/dsh-mneme/version-check")
|
|
2140
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
2141
|
+
.then((j) => {
|
|
2142
|
+
if (!cancelled && j && j.status === "outdated" && typeof j.latest === "string" && typeof j.version === "string") {
|
|
2143
|
+
setUpdateInfo({ latest: j.latest, current: j.version });
|
|
2144
|
+
}
|
|
2145
|
+
})
|
|
2146
|
+
.catch(() => {});
|
|
2147
|
+
return () => { cancelled = true; };
|
|
2148
|
+
}, []);
|
|
2149
|
+
|
|
2116
2150
|
// 反馈链接预填:环境信息(插件版本 + 平台)+ 问题描述骨架。邮件主题带
|
|
2117
2151
|
// 版本/平台方便归档,正文同款模板;纯链接零后端成本。
|
|
2118
2152
|
const feedbackEnv = `**插件版本**: ${pkgVersion}\n**平台**: ${platformLabel()}\n`;
|
|
@@ -2323,6 +2357,17 @@ window.__ModuleLoader__.load({
|
|
|
2323
2357
|
}
|
|
2324
2358
|
|
|
2325
2359
|
return h("div", null,
|
|
2360
|
+
// 版本自检横幅 — 仅 outdated 时渲染(up-to-date/ahead/unknown/失败
|
|
2361
|
+
// 全部零渲染)。钉子警示:安装时指定过版本号的 profile 会被 pnpm
|
|
2362
|
+
// 挡住常规升级(#174 报障者的实际成因);市场收录新发布约有 1 天延迟。
|
|
2363
|
+
updateInfo && h("section", { className: "mneme-set-sec" },
|
|
2364
|
+
h("div", { className: "mneme-set-title" },
|
|
2365
|
+
t("memory.settings.version.outdated")
|
|
2366
|
+
.replace("{v}", updateInfo.latest)
|
|
2367
|
+
.replace("{c}", updateInfo.current)
|
|
2368
|
+
),
|
|
2369
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.settings.version.outdatedHint"))
|
|
2370
|
+
),
|
|
2326
2371
|
// 用户画像 — who the agent is talking to
|
|
2327
2372
|
h("section", { className: "mneme-set-sec" },
|
|
2328
2373
|
h("div", { className: "mneme-set-title" }, t("memory.settings.profile")),
|
|
@@ -3191,11 +3236,30 @@ window.__ModuleLoader__.load({
|
|
|
3191
3236
|
// 右侧滑出:全文、来源、质量分、关联实体与手动操作(编辑/归档/删除)。
|
|
3192
3237
|
// 编辑态本地暂存草稿,保存经 POST /update 落库;后端成功后重渲染镜像,
|
|
3193
3238
|
// 前端只做本地视图同步。两步删除确认在此完成(红钮武装 → 实心红提交)。
|
|
3239
|
+
// v0.8.1 底座(issue #170):scope 行的来源徽注(显式/自动)。存量行无来源
|
|
3240
|
+
// 时不加后缀——与 A4 的「未标注零视觉变化」同款克制。
|
|
3241
|
+
const scopeSourceSuffix = (t, source) =>
|
|
3242
|
+
source === "explicit" ? ` · ${t("memory.explorer.detail.scopeSourceExplicit")}`
|
|
3243
|
+
: source === "auto" ? ` · ${t("memory.explorer.detail.scopeSourceAuto")}`
|
|
3244
|
+
: "";
|
|
3245
|
+
const scopeProvenanceTitle = (t, source, decidedAt) =>
|
|
3246
|
+
[
|
|
3247
|
+
source === "explicit" ? t("memory.explorer.detail.scopeSourceExplicit")
|
|
3248
|
+
: source === "auto" ? t("memory.explorer.detail.scopeSourceAuto") : null,
|
|
3249
|
+
decidedAt ?? null
|
|
3250
|
+
].filter(Boolean).join(" · ");
|
|
3251
|
+
|
|
3194
3252
|
function MemoryDrawer({ t, memory, conflict, deleting, deleteError, onClose, onDelete, onSaved }) {
|
|
3195
3253
|
const [editing, setEditing] = useState(false);
|
|
3196
3254
|
const [title, setTitle] = useState(memory.title || "");
|
|
3197
3255
|
const [content, setContent] = useState(memory.content || "");
|
|
3198
3256
|
const [importance, setImportance] = useState(memory.importance || 3);
|
|
3257
|
+
// v0.8.1 底座(issue #170):scope 归属的人工修正入口。空串=放宽到全局
|
|
3258
|
+
// (发送 null),非空=收窄到该标签;只有真的改动才会随 patch 发送并盖
|
|
3259
|
+
// explicit 章——普通编辑不得污染归属来源。
|
|
3260
|
+
const [agentScope, setAgentScope] = useState(memory.agent_scope || "");
|
|
3261
|
+
const [workspaceScope, setWorkspaceScope] = useState(memory.workspace_scope || "");
|
|
3262
|
+
const [confirmWiden, setConfirmWiden] = useState(false);
|
|
3199
3263
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
3200
3264
|
const [busy, setBusy] = useState(false);
|
|
3201
3265
|
const [savedTick, setSavedTick] = useState(false);
|
|
@@ -3234,7 +3298,25 @@ window.__ModuleLoader__.load({
|
|
|
3234
3298
|
const save = () => {
|
|
3235
3299
|
const t2 = title.trim();
|
|
3236
3300
|
if (!t2) return;
|
|
3237
|
-
|
|
3301
|
+
const patch = { title: t2, content, importance };
|
|
3302
|
+
const curAgent = memory.agent_scope ?? "";
|
|
3303
|
+
const curWorkspace = memory.workspace_scope ?? "";
|
|
3304
|
+
const nextAgent = agentScope.trim();
|
|
3305
|
+
const nextWorkspace = workspaceScope.trim();
|
|
3306
|
+
const agentChanged = nextAgent !== curAgent;
|
|
3307
|
+
const workspaceChanged = nextWorkspace !== curWorkspace;
|
|
3308
|
+
if (agentChanged) patch.agent_scope = nextAgent || null;
|
|
3309
|
+
if (workspaceChanged) patch.workspace_scope = nextWorkspace || null;
|
|
3310
|
+
// 放宽可见性(标注→全局)必须显式确认(issue #170 的安全阀):
|
|
3311
|
+
// 第一次点保存只弹确认,再次点「确认放宽」才真正落库。
|
|
3312
|
+
const widening = (curAgent && agentChanged && !nextAgent)
|
|
3313
|
+
|| (curWorkspace && workspaceChanged && !nextWorkspace);
|
|
3314
|
+
if (widening && !confirmWiden) {
|
|
3315
|
+
setConfirmWiden(true);
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
setConfirmWiden(false);
|
|
3319
|
+
postUpdate(patch, {});
|
|
3238
3320
|
};
|
|
3239
3321
|
|
|
3240
3322
|
return h("aside", { className: "mneme-drawer", "aria-label": t("memory.explorer.detail") },
|
|
@@ -3278,12 +3360,40 @@ window.__ModuleLoader__.load({
|
|
|
3278
3360
|
memory.occurred_at && h(react.Fragment, null,
|
|
3279
3361
|
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.occurred")),
|
|
3280
3362
|
h("span", { className: "mneme-dmetaval", title: memory.occurred_at }, formatDateShort(memory.occurred_at))),
|
|
3281
|
-
|
|
3363
|
+
// v0.8.1 底座:scope 行双态——查看态展示归属 + 来源徽注(显式声明的
|
|
3364
|
+
// 「全局」也渲染,不再因 NULL 而隐身);编辑态为文本框(留空=全局)。
|
|
3365
|
+
(editing || memory.agent_scope || memory.agent_scope_source === "explicit") && h(react.Fragment, null,
|
|
3282
3366
|
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.agentScope")),
|
|
3283
|
-
|
|
3284
|
-
|
|
3367
|
+
editing
|
|
3368
|
+
? h("input", {
|
|
3369
|
+
type: "text",
|
|
3370
|
+
className: "mneme-select",
|
|
3371
|
+
style: { height: 26, fontSize: 12, justifySelf: "start", width: "100%" },
|
|
3372
|
+
value: agentScope,
|
|
3373
|
+
placeholder: t("memory.explorer.detail.scopeGlobal"),
|
|
3374
|
+
title: t("memory.explorer.detail.scopeEditHint"),
|
|
3375
|
+
onChange: (e) => setAgentScope(e.target.value)
|
|
3376
|
+
})
|
|
3377
|
+
: h("span", {
|
|
3378
|
+
className: "mneme-dmetaval",
|
|
3379
|
+
title: scopeProvenanceTitle(t, memory.agent_scope_source, memory.scope_decided_at) || memory.agent_scope
|
|
3380
|
+
}, (memory.agent_scope ?? t("memory.explorer.detail.scopeGlobal")) + scopeSourceSuffix(t, memory.agent_scope_source))),
|
|
3381
|
+
(editing || memory.workspace_scope || memory.workspace_scope_source === "explicit") && h(react.Fragment, null,
|
|
3285
3382
|
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.workspaceScope")),
|
|
3286
|
-
|
|
3383
|
+
editing
|
|
3384
|
+
? h("input", {
|
|
3385
|
+
type: "text",
|
|
3386
|
+
className: "mneme-select",
|
|
3387
|
+
style: { height: 26, fontSize: 12, justifySelf: "start", width: "100%" },
|
|
3388
|
+
value: workspaceScope,
|
|
3389
|
+
placeholder: t("memory.explorer.detail.scopeGlobal"),
|
|
3390
|
+
title: t("memory.explorer.detail.scopeEditHint"),
|
|
3391
|
+
onChange: (e) => setWorkspaceScope(e.target.value)
|
|
3392
|
+
})
|
|
3393
|
+
: h("span", {
|
|
3394
|
+
className: "mneme-dmetaval",
|
|
3395
|
+
title: scopeProvenanceTitle(t, memory.workspace_scope_source, memory.scope_decided_at) || memory.workspace_scope
|
|
3396
|
+
}, (memory.workspace_scope ?? t("memory.explorer.detail.scopeGlobal")) + scopeSourceSuffix(t, memory.workspace_scope_source))),
|
|
3287
3397
|
memory.sensitivity && h(react.Fragment, null,
|
|
3288
3398
|
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.sensitivity")),
|
|
3289
3399
|
h("span", { className: "mneme-dmetaval", title: memory.sensitivity }, memory.sensitivity)),
|
|
@@ -3316,7 +3426,16 @@ window.__ModuleLoader__.load({
|
|
|
3316
3426
|
h("div", { className: "mneme-dactions" },
|
|
3317
3427
|
editing
|
|
3318
3428
|
? h(react.Fragment, null,
|
|
3319
|
-
|
|
3429
|
+
confirmWiden
|
|
3430
|
+
? h(react.Fragment, null,
|
|
3431
|
+
h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-warn,#b8860b)" } },
|
|
3432
|
+
t("memory.explorer.detail.scopeWidenHint")),
|
|
3433
|
+
h("button", { type: "button", className: "mneme-btn mneme-btndangerconfirm", disabled: busy, onClick: save },
|
|
3434
|
+
t("memory.explorer.detail.scopeWidenConfirm")),
|
|
3435
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => setConfirmWiden(false) },
|
|
3436
|
+
t("memory.explorer.cancel"))
|
|
3437
|
+
)
|
|
3438
|
+
: h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: save }, t("memory.explorer.detail.save")),
|
|
3320
3439
|
h("button", {
|
|
3321
3440
|
type: "button",
|
|
3322
3441
|
className: "mneme-btn",
|
|
@@ -3326,6 +3445,9 @@ window.__ModuleLoader__.load({
|
|
|
3326
3445
|
setTitle(memory.title || "");
|
|
3327
3446
|
setContent(memory.content || "");
|
|
3328
3447
|
setImportance(memory.importance || 3);
|
|
3448
|
+
setAgentScope(memory.agent_scope || "");
|
|
3449
|
+
setWorkspaceScope(memory.workspace_scope || "");
|
|
3450
|
+
setConfirmWiden(false);
|
|
3329
3451
|
}
|
|
3330
3452
|
}, t("memory.explorer.detail.cancel")),
|
|
3331
3453
|
savedTick && h("span", { className: "mneme-saved" }, t("memory.explorer.detail.saved")),
|
package/lib/dream/sleep.js
CHANGED
|
@@ -4,6 +4,8 @@ import { STR, langOf } from "../lang.js";
|
|
|
4
4
|
// 1. conflict resolution — high-similarity same-type pairs are either parked
|
|
5
5
|
// for review (freeze mode) or adjudicated by the LLM (winner kept / loser
|
|
6
6
|
// archived), reusing the dream conflict machinery. Strictness-graded.
|
|
7
|
+
// v0.8.1 (issue #170): cross-scope pairs are ALWAYS parked for human
|
|
8
|
+
// ownership review with a dedicated reason — never auto-adjudicated.
|
|
7
9
|
// 2. archival demotion — memories unreferenced past sleepArchiveDays shrink
|
|
8
10
|
// to a one-line summary with the full body moved to _full_content; past
|
|
9
11
|
// sleepCompressDays they are archived outright.
|
|
@@ -18,6 +20,7 @@ import { STR, langOf } from "../lang.js";
|
|
|
18
20
|
import { randomUUID, createHash } from "node:crypto";
|
|
19
21
|
import { validateDecisions, applyDecisions, ACTIONS } from "./decisions.js";
|
|
20
22
|
import { findPotentialConflicts, cosineSimilarity } from "./clustering.js";
|
|
23
|
+
import { scopeKeyOf } from "../scope.js";
|
|
21
24
|
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback, maintainIndexAfterDream } from "../dream.js";
|
|
22
25
|
import { computeHeat } from "../heat.js";
|
|
23
26
|
|
|
@@ -169,29 +172,57 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
169
172
|
selected.push(p);
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
//
|
|
175
|
+
// v0.8.1(issue #170 第 2 步):跨 scope 相似对分流。同一内容落在两个归属下
|
|
176
|
+
// (任一维标注键不等即算,NULL=未标注=全局也参与比较——「全局副本 + 专属副本
|
|
177
|
+
// 并存」正是收窄/提升裁决的典型候选)不能自动裁决:归档败者可能销毁该内容在
|
|
178
|
+
// 某个归属下的唯一副本,LLM 也无从替用户决定归属。一律停车到冲突待确认队列
|
|
179
|
+
// (专属 reason),同 scope 对维持既有路径(freeze 停车 / LLM 裁决)。
|
|
180
|
+
const isCrossScopePair = (p) =>
|
|
181
|
+
scopeKeyOf(p.a.agent_scope) !== scopeKeyOf(p.b.agent_scope)
|
|
182
|
+
|| scopeKeyOf(p.a.workspace_scope) !== scopeKeyOf(p.b.workspace_scope);
|
|
183
|
+
const crossPairs = selected.filter(isCrossScopePair);
|
|
184
|
+
const samePairs = selected.filter((p) => !isCrossScopePair(p));
|
|
185
|
+
const parkPair = (p, reason) => {
|
|
186
|
+
try {
|
|
187
|
+
// 返回 undefined = 该对已被人工裁决且两侧内容未变(复核项 4)——不算停车。
|
|
188
|
+
return service.saveConflictPending({ run_id: runId, memory_a: p.a.id, memory_b: p.b.id, reason }) != null;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger?.warn?.(`dsh-mneme sleep: failed to park conflict ${p.a.id}/${p.b.id}: ${String(error)}`);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const scopeReason = (p) => STR.scopeCandidateReason[language](p.similarity.toFixed(2));
|
|
195
|
+
const simReason = (p) => STR.similarityReason[language](p.similarity.toFixed(2));
|
|
196
|
+
|
|
197
|
+
// Freeze mode: park pairs for manual review, no LLM required. Cross-scope
|
|
198
|
+
// pairs carry the dedicated scope-candidate reason so the queue reads them
|
|
199
|
+
// as ownership decisions rather than plain duplicates.
|
|
173
200
|
if (config.conflictFreezeEnabled === true) {
|
|
174
201
|
let frozen = 0;
|
|
175
202
|
for (const p of selected) {
|
|
176
|
-
|
|
177
|
-
service.saveConflictPending({ run_id: runId, memory_a: p.a.id, memory_b: p.b.id, reason: STR.similarityReason[language](p.similarity.toFixed(2)) });
|
|
178
|
-
frozen++;
|
|
179
|
-
} catch (error) {
|
|
180
|
-
logger?.warn?.(`dsh-mneme sleep: failed to freeze conflict ${p.a.id}/${p.b.id}: ${String(error)}`);
|
|
181
|
-
}
|
|
203
|
+
if (parkPair(p, isCrossScopePair(p) ? scopeReason(p) : simReason(p))) frozen++;
|
|
182
204
|
}
|
|
183
|
-
return { status: frozen > 0 ? "ok" : "noop", frozen, pairs: selected.length };
|
|
205
|
+
return { status: frozen > 0 ? "ok" : "noop", frozen, scopeCandidates: crossPairs.length, pairs: selected.length };
|
|
184
206
|
}
|
|
185
207
|
|
|
186
|
-
// LLM
|
|
208
|
+
// 非 freeze:跨 scope 对先停车(绝不进 LLM 裁决),同 scope 对照常走 LLM。
|
|
209
|
+
let scopeParked = 0;
|
|
210
|
+
for (const p of crossPairs) {
|
|
211
|
+
if (parkPair(p, scopeReason(p))) scopeParked++;
|
|
212
|
+
}
|
|
213
|
+
if (samePairs.length === 0) {
|
|
214
|
+
return { status: scopeParked > 0 ? "ok" : "noop", frozen: 0, scopeCandidates: scopeParked, pairs: selected.length };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// LLM adjudication(仅同 scope 对)。
|
|
187
218
|
const route = resolveSleepRoute(ctx, config, logger);
|
|
188
|
-
if (!route) return { status: "skipped", reason: "no llm route" };
|
|
219
|
+
if (!route) return { status: "skipped", reason: "no llm route", scopeCandidates: scopeParked, pairs: selected.length };
|
|
189
220
|
const snapshot = new Map();
|
|
190
|
-
for (const p of
|
|
221
|
+
for (const p of samePairs) {
|
|
191
222
|
snapshot.set(p.a.id, p.a);
|
|
192
223
|
snapshot.set(p.b.id, p.b);
|
|
193
224
|
}
|
|
194
|
-
const listText =
|
|
225
|
+
const listText = samePairs.map((p) =>
|
|
195
226
|
STR.candidateConflicts[language](p)
|
|
196
227
|
).join("\n\n");
|
|
197
228
|
const sleepEffort = await resolveDreamEffort(ctx, route, config.sleepReasoningEffort, logger);
|
package/lib/dream.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts, cosineSimilarity } from "./dream/clustering.js";
|
|
3
|
+
import { scopeKeyOf } from "./scope.js";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
import { STR, langOf } from "./lang.js";
|
|
5
6
|
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure, resolveDreamEffort, resolveRoute };
|
|
@@ -989,7 +990,21 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
989
990
|
// bounds the review queue; overflow is skipped with a warning.
|
|
990
991
|
let frozenCount = 0;
|
|
991
992
|
const frozenIds = [];
|
|
992
|
-
|
|
993
|
+
// v0.8.1(issue #170 复核项 3):跨 scope 的 conflict 决策两种模式都一律
|
|
994
|
+
// 停车人工裁决、绝不自动归档——否则普通 dream 的 LLM 能归档别的 scope 的
|
|
995
|
+
// 唯一副本,sleep 路径(scope-candidate-queue)的保护在这里缺位。同 scope
|
|
996
|
+
// 的 conflict 决策照旧:freeze 停车 / 非 freeze 自动裁决。
|
|
997
|
+
const isCrossScopeDecision = (d) => {
|
|
998
|
+
const a = snapshot.get(d.winner);
|
|
999
|
+
const b = snapshot.get(d.loser);
|
|
1000
|
+
return !!a && !!b && (
|
|
1001
|
+
scopeKeyOf(a.agent_scope) !== scopeKeyOf(b.agent_scope)
|
|
1002
|
+
|| scopeKeyOf(a.workspace_scope) !== scopeKeyOf(b.workspace_scope)
|
|
1003
|
+
);
|
|
1004
|
+
};
|
|
1005
|
+
const applyList = freezeEnabled
|
|
1006
|
+
? decisions.filter((d) => d.action !== "conflict")
|
|
1007
|
+
: decisions.filter((d) => d.action !== "conflict" || !isCrossScopeDecision(d));
|
|
993
1008
|
if (freezeEnabled) {
|
|
994
1009
|
const conflictsToFreeze = decisions.filter((d) => d.action === "conflict");
|
|
995
1010
|
if (conflictsToFreeze.length > 0) {
|
|
@@ -1003,7 +1018,10 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1003
1018
|
}
|
|
1004
1019
|
for (const d of toFreeze) {
|
|
1005
1020
|
try {
|
|
1006
|
-
|
|
1021
|
+
// 返回 undefined = 该对已被人工裁决且两侧内容未变(issue #170 复核
|
|
1022
|
+
// 项 4:内容变了才重新入队)——不算停车、不进 frozenIds。
|
|
1023
|
+
const parked = service.saveConflictPending({ run_id: runId, memory_a: d.winner, memory_b: d.loser, reason: d.reason });
|
|
1024
|
+
if (parked == null) continue;
|
|
1007
1025
|
frozenCount++;
|
|
1008
1026
|
frozenIds.push(d.winner, d.loser);
|
|
1009
1027
|
} catch (error) {
|
|
@@ -1014,6 +1032,29 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1014
1032
|
logger?.warn?.(`dsh-mneme dream: conflict freeze lookup failed: ${String(error)}`);
|
|
1015
1033
|
}
|
|
1016
1034
|
}
|
|
1035
|
+
} else {
|
|
1036
|
+
// 非 freeze:跨 scope conflict 停车(绝不进 applyDecisions)。带专属
|
|
1037
|
+
// reason + 模型给出的裁决理由,队列里读起来是「归属裁决」而非重复项。
|
|
1038
|
+
const crossScope = decisions.filter((d) => d.action === "conflict" && isCrossScopeDecision(d));
|
|
1039
|
+
for (const d of crossScope) {
|
|
1040
|
+
try {
|
|
1041
|
+
const parked = service.saveConflictPending({
|
|
1042
|
+
run_id: runId,
|
|
1043
|
+
memory_a: d.winner,
|
|
1044
|
+
memory_b: d.loser,
|
|
1045
|
+
reason: STR.scopeConflictParkReason[language](d.reason)
|
|
1046
|
+
});
|
|
1047
|
+
if (parked != null) {
|
|
1048
|
+
frozenCount++;
|
|
1049
|
+
frozenIds.push(d.winner, d.loser);
|
|
1050
|
+
}
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
logger?.warn?.(`dsh-mneme dream: failed to park cross-scope conflict ${d.winner}/${d.loser}: ${String(error)}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (crossScope.length > 0) {
|
|
1056
|
+
logger?.info?.(`[dsh-mneme] dream: parked ${crossScope.length} cross-scope conflict(s) for human ownership review`);
|
|
1057
|
+
}
|
|
1017
1058
|
}
|
|
1018
1059
|
|
|
1019
1060
|
// CAS-guarded, per-decision-transactional apply against the run snapshot:
|
package/lib/lang.js
CHANGED
|
@@ -103,6 +103,17 @@ export const STR = {
|
|
|
103
103
|
|
|
104
104
|
// --- dream/sleep.js:冲突候选列表 --------------------------------------------
|
|
105
105
|
similarityReason: { zh: (s) => `相似度 ${s}`, en: (s) => `similarity ${s}` },
|
|
106
|
+
// v0.8.1(issue #170 第 2 步):跨 scope 相似对停车到冲突队列的专属 reason。
|
|
107
|
+
scopeCandidateReason: {
|
|
108
|
+
zh: (s) => `跨作用域相似 ${s}(疑似同一内容落在两个归属下,请裁决归属/去重)`,
|
|
109
|
+
en: (s) => `cross-scope similar ${s} (same content under two scopes — review ownership)`
|
|
110
|
+
},
|
|
111
|
+
// v0.8.1(issue #170 复核项 3):普通 dream 的 LLM 输出跨 scope conflict 时
|
|
112
|
+
// 停车用(相似度未知,带模型给的裁决理由)。
|
|
113
|
+
scopeConflictParkReason: {
|
|
114
|
+
zh: (r) => `跨作用域冲突(归属不同,不自动裁决)${r ? `:${r}` : ""}`,
|
|
115
|
+
en: (r) => `cross-scope conflict (different ownership — not auto-adjudicated)${r ? `: ${r}` : ""}`
|
|
116
|
+
},
|
|
106
117
|
candidateConflicts: {
|
|
107
118
|
zh: (p) => `候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`,
|
|
108
119
|
en: (p) => `Candidate conflicts:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(similarity ${p.similarity.toFixed(2)})`
|
package/lib/scope.js
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
// 硬约束:解析失败绝不阻塞写入——任何一步取不到就落 NULL(读侧 NULL = 未标注
|
|
9
9
|
// = 全局可见),任何异常都被吞掉(registry 故障 warn 一次后降级)。
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// v0.8.1(issue #170 复核项 2):scopeEnabled 的闸门从解析器挪到「写标注」处
|
|
12
|
+
// (memory_save 的 auto 分支)。解析器恒返回会话身份(真取不到才是 null)——
|
|
13
|
+
// 读取侧(strictVisibility / memory_get / inject)与显式声明都依赖它:flag 关
|
|
14
|
+
// 只关「自动标注」,不能连读取身份一起关,否则 strictScope 硬墙被静默拆掉
|
|
15
|
+
// (fail-open)。
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* @param {object} deps
|
|
@@ -31,8 +34,6 @@ export function createScopeResolver({ ctx, config, logger } = {}) {
|
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
return function resolveWriteScope(exec) {
|
|
34
|
-
if (config?.scopeEnabled !== true) return null;
|
|
35
|
-
|
|
36
37
|
let session = null;
|
|
37
38
|
try {
|
|
38
39
|
session = exec?.agent?.session ?? null;
|
|
@@ -81,3 +82,37 @@ export function createScopeResolver({ ctx, config, logger } = {}) {
|
|
|
81
82
|
return { agent_scope: agentScope, workspace_scope: workspaceScope };
|
|
82
83
|
};
|
|
83
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* v0.8.1 底座(issue #170):显式 scope 参数(memory_save / memory_update /
|
|
88
|
+
* 面板编辑)的归一化。约定:
|
|
89
|
+
* - 省略(undefined,由调用方判断)= 该维不声明,回落载体自动标注;
|
|
90
|
+
* - "global"(大小写不敏感)/ "*" / 空白 = 显式声明全局 → 存储 NULL
|
|
91
|
+
* (行上的 agent_scope_source='explicit' 承担「显式全局」与「从未标注」
|
|
92
|
+
* 的区分,NULL 本身继续表示全局可见);
|
|
93
|
+
* - 其余非空 trim 字符串 = 收窄到该标签。
|
|
94
|
+
* 脏输入(非字符串)落 NULL(等同声明全局)——与 resolveWriteScope 的「解析
|
|
95
|
+
* 绝不阻塞写入」同一哲学:显式声明是用户意图,形状不对时宁可放宽不报错。
|
|
96
|
+
*
|
|
97
|
+
* @param {unknown} raw 工具/HTTP 参数原值
|
|
98
|
+
* @returns {string|null} 归一化后的标签值(null=显式全局)
|
|
99
|
+
*/
|
|
100
|
+
export function normalizeExplicitScope(raw) {
|
|
101
|
+
if (typeof raw !== "string") return null;
|
|
102
|
+
const s = raw.trim();
|
|
103
|
+
if (!s || s === "*" || s.toLowerCase() === "global") return null;
|
|
104
|
+
return s;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* scope 标注的比较键(去重键/检索加权/跨 scope 配对共用口径):非空 trim 字符串
|
|
109
|
+
* 原样返回,其余(NULL/undefined/空串/非字符串)统一为 null(「未标注」)。
|
|
110
|
+
* v0.8.1(issue #170 第 2 步)从 service.js 上提为共享定义——sleep 的跨 scope
|
|
111
|
+
* 候选配对必须与去重键用同一把钥匙,NULL 与已标注才不会错配。
|
|
112
|
+
*
|
|
113
|
+
* @param {unknown} v scope 标注原值
|
|
114
|
+
* @returns {string|null} 比较键(null=未标注)
|
|
115
|
+
*/
|
|
116
|
+
export function scopeKeyOf(v) {
|
|
117
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
118
|
+
}
|