diff --git a/AGENTS.md b/AGENTS.md index b63abda..292556a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,8 @@ dsh-mneme 是 DSH 宿主的记忆插件(蒸馏 / 注入 / 检索 / 巩固 / sc | 回收 HTTP 口径 | `src/reclaim-http.js` | 面板路由与 Bearer 数据面共用的唯一一份解析与答复(#411);两张皮只许剩鉴权与路径不同 | | 服务层 | `src/service.js` | saveWithDedupe(去重键 type+title+scope 三元组)、fuseRecall 检索融合(keyword/vector/bm25/entity)、注入候选、镜像同步、冲突队列 | | 写入准入 | `src/write-admission.js` | 写入前的会话写入预算与同话题冷却(#254)。第 1 级确定性判据(空白 / 噪声 / 密钥 PII)只在三个条件同时成立时才跑:有 `sessionKey`、`llmAudit.enabled` 未被显式关、`writeAdmission.enabled === true`;都成立且**命中**时,再由 `writeAdmission.enforce` 决定是否真拒绝(`ADMISSION_DECISION_DENY`)。只开 `enforce` 不跑判据、也不拦写入。默认只计量,测量点落 `llm_audit_logs` | -| 注入 | `src/inject.js` | 注入位构造、内容截断、跨轮轮换 | +| 注入 | `src/inject.js` + `src/user-settings.js` | 注入位构造、内容截断、跨轮轮换;「用户设置」块(人格 → 画像 → 规则)的渲染与寻址住在 user-settings.js,**面板预览共用这一份**(前端不自己拼第二份) | +| 整理与画像草稿 | `src/organize.js` + `src/profile-extract.js` | agent 主动整理(dryRun 比对报告 → agent 判断 → apply)与从记忆提取画像草稿(只回草稿,绝不写 `user_settings`)。整理里有两种「不要它」,别混:**`discard`** = 这条候选不写,任何东西都不动(只进回执);**`archive`** = 把报告标为被取代的那条既有行归档——可恢复,绝不物理删除。两个入口都 opt-in 默认关(`organizeEnabled` / `profileExtractEnabled`) | | 蒸馏 | `src/summarize.js` + `src/quality-filter.js` | 会话 → 记忆;质量打分与处置(归档/降权) | | 巩固与睡眠 | `src/dream.js` + `src/dream/{decisions,clustering,sleep}.js` | LLM 巩固决策、dream_runs 审计回执 | | 实体 | `src/entities/extractor.js`(存储侧在 store 三表) | 写入时 LLM 抽取实体/属性/关系 | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4464ed1..e7d9e4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,7 +148,7 @@ Bug-fix PRs for existing desktop compatibility issues are still welcome. Versioning follows semantic versioning (`MAJOR.MINOR.PATCH`). The mechanical parts are automated; the editorial parts stay with a human — **the machine writes what the version number alone determines, a human writes what needs judgement**. Full flow: -1. **Run the Release Prep workflow** (`.github/workflows/release-prep.yml`, manual dispatch, one input: the target version). It bumps `version` in `dsh-mneme/package.json` + `package-lock.json`, refreshes the test count in both READMEs (badge + the four `# N …tests` comment shapes), opens the new CHANGELOG section under `[Unreleased]` and moves whatever had accumulated there into it (a `- (待填)` placeholder is only left when `[Unreleased]` was empty), adds placeholder rows to the version tables in both READMEs, flips a leftover 「🚧 准备中」 roadmap cell to released, and opens a `release-prep/X.Y.Z` PR. +1. **Run the Release Prep workflow** (`.github/workflows/release-prep.yml`, manual dispatch, one input: the target version). It bumps `version` in `dsh-mneme/package.json` + `package-lock.json`, refreshes the test count in both READMEs (badge + the four `# N …tests` comment shapes), opens the new CHANGELOG section under `[Unreleased]` and moves whatever had accumulated there into it (a `- (待填)` placeholder is only left when `[Unreleased]` was empty), adds placeholder rows to the version tables in both READMEs, flips a leftover 「🚧 准备中」 roadmap cell to released, and opens a `release-prep/X.Y.Z` PR. **The last step needs the repo-level toggle** Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"; with it off the run fails there **after** the branch was pushed (verified on v0.9.4, 2026-10-21) — don't re-run the workflow, just open the PR yourself from that branch (`gh pr create --base main --head release-prep/X.Y.Z`); the machine output is already on the branch. 2. **Fill in what only a human can write**, in that PR: - the wording and the cuts in the CHANGELOG entries (`[Unreleased]` entries are already moved into the new section by the workflow — do not move them by hand; split them into subsections and rewrite them for outside readers). End the section with a `### 贡献者 / Thanks` subsection summarising outside contributors (`git log vPrev..HEAD --format='%an <%ae>' | sort -u`; list non-maintainers as "GitHub handle + what they did", and keep the inline 「感谢 @handle」 in the entry too); omit it when there are none. **Keep entries neutral**: no references to non-public issue numbers and no internal experiment codenames — nobody reading this repository can resolve them. - the theme text of the new placeholder rows in both READMEs; @@ -346,7 +346,7 @@ DSH 上游仍处于 developer preview 阶段,API 与服务接口变动频繁 版本号遵循语义化版本(`MAJOR.MINOR.PATCH`)。机械的部分交给工作流,编辑判断的部分留给人——**版本号自己能确定的东西机器写,需要判断的东西人写**。完整流程: -1. **跑 Release Prep 工作流**(`.github/workflows/release-prep.yml`,手动触发,只填目标版本号)。它做这些:改 `dsh-mneme/package.json` 与 `package-lock.json` 的 `version`;刷新两个 README 的测试数(徽章 + 四种 `# N …测试` 注释形状);在 `[Unreleased]` 下面开新版本节,并把里面攒的条目整段搬进去(只有 `[Unreleased]` 本来是空的才留一条 `- (待填)`);给两个 README 的版本表各插一行占位;把路线图上遗留的「🚧 准备中」回填成已发布;最后推 `release-prep/X.Y.Z` 分支并开 PR。 +1. **跑 Release Prep 工作流**(`.github/workflows/release-prep.yml`,手动触发,只填目标版本号)。它做这些:改 `dsh-mneme/package.json` 与 `package-lock.json` 的 `version`;刷新两个 README 的测试数(徽章 + 四种 `# N …测试` 注释形状);在 `[Unreleased]` 下面开新版本节,并把里面攒的条目整段搬进去(只有 `[Unreleased]` 本来是空的才留一条 `- (待填)`);给两个 README 的版本表各插一行占位;把路线图上遗留的「🚧 准备中」回填成已发布;最后推 `release-prep/X.Y.Z` 分支并开 PR。**最后那一步吃仓库级开关**:Settings → Actions → General →「Allow GitHub Actions to create and approve pull requests」要打开,否则分支已经推上去了、PR 却开不出来(v0.9.4 于 2026-10-21 就是这样)。这时**别重跑工作流**——机器产出已经在那条分支上了,人直接 `gh pr create --base main --head release-prep/X.Y.Z` 补开就行。 2. **在这个 PR 里补上只有人能写的内容**: - CHANGELOG 条目的措辞与取舍(`[Unreleased]` 攒的条目工作流已整段搬进新版本节,**不用手搬**;把它们分节并改写成外部读者能看懂的话)。小节末尾加 `### 贡献者 / Thanks` 汇总本版外部贡献者(`git log v上一版..HEAD --format='%an <%ae>' | sort -u`,非维护者逐条列「GitHub 用户名 + 一句话做了什么」,条目内也保留「感谢 @用户名」);无外部贡献者时省掉该小节。**条目一律用中性措辞**:不引用非公开的 issue 编号、不出现内部实验代号——它们在公开仓库里解析不出来,读者只会困惑。 - 两个 README 里新占位行的主题描述; diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index d11f8d9..5ad51e2 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -49,7 +49,8 @@ dsh web - 逐 type 记录 `committed / failed / pending` 回执,健康端点区分 `ok / degraded / unknown` - 状态写失败不静默:同步失败落日志并留债务,重启自动收敛 -### 模型工具(10 个) +### 模型工具(默认注册 11 个;开 `organizeEnabled` 后 12 个) + | 工具 | 功能 | |------|------| @@ -61,7 +62,9 @@ dsh web | `memory_delete` | 删除记忆(按记忆 ID 精确删除) | | `memory_forget` | 抑制注入(降权不删除,可恢复) | | `memory_archive` | 归档/恢复记忆(v0.2.5;归档后隐藏于列表/搜索/注入/整理,`archived=false` 可恢复) | +| `set_scope_state` | 按下/松开某个作用域的**可逆闸门**(#471:政策轴。暂停期间该作用域的记忆照旧存着,只是不注入、不参与巩固,松开即原样回来;不改任何一条记忆的标注,与上面逐条操作的 `memory_forget` / `memory_archive` 是两根不同的轴)。两级开关 `scopeState.enabled` / `.enforce` 默认都关,没开时按下不产生拦截效果 | | `memory_register_document` | 登记 agent 产长文档的指针行(#230:摘要 + `doc_path` + evidence 落 `document` 类型;正文留在磁盘、由 agent 自管,注册口唯一,旁路写入被拒) | +| `memory_organize` | 主动整理(#231,**要 `organizeEnabled` 打开才注册**):`mode=dryRun` 先出比对报告(每条候选 `exact` / `near` / `new` + 命中行,**不写一行库**、只留审计回执),agent 判断后 `mode=apply` 引用那份报告的 `run_id` 落地。两种「不要它」要分清——**`discard`** = 这条候选不写,什么都不动(只进回执);**`archive`** = 把报告标为被取代的那条**既有行**归档(可恢复,绝不物理删除)。没有比对过的候选写不进库,一份报告也只许落地一次 | | `memory_runtime` | 本地推理运行时的状态 / 供给 / 校验(v0.7.32,自管运行时三档取件) | ### 工作流(M.O.,v0.9 beta) @@ -645,7 +648,7 @@ Claude Code 挂载示例(项目根 `.mcp.json`;token 在面板「设置 → │ 服务层:saveWithDedupe / injectCandidates │ │ / mergeHumanEdits / onWrite 钩子 │ ├─────────────────────────────────────────────────┤ -│ 模型接口:10 个工具 + 自动注入 + 会话摘要 │ +│ 模型接口:11 个工具 + 自动注入 + 会话摘要 │ ├─────────────────────────────────────────────────┤ │ autoDream:阈值调度 → LLM 决策清单 │ │ → 校验(fail-safe)→ 应用 → 摘要 │ @@ -663,7 +666,10 @@ src/ ├── service.js # 领域逻辑(去重合并、注入筛选、写入钩子、scope 加权/硬过滤、冲突队列) ├── scope.js # 作用域解析(agentPreset + workspace registry 反查,issue #17) ├── config.js # schemastery 配置 schema -├── tools.js # 10 个模型工具(defineTool) +├── tools.js # 11 个模型工具(defineTool;memory_organize 要 organizeEnabled 打开才注册,开了是 12 个) +├── user-settings.js # 「用户设置」注入块(人格 → 画像 → 规则)的唯一渲染处,注入与面板预览共用 +├── profile-extract.js # 从活跃记忆提取「助手眼中的用户」画像草稿(只出草稿,不写 user_settings) +├── organize.js # agent 主动整理:dryRun 比对报告 → agent 判断 → apply 落库(筛除=归档不删,留 dream_runs 回执) ├── inject.js # systemPrompt.context 动态注入(含花括号转义) ├── summarize.js # 会话结束 LLM 摘要 ├── dream.js # autoDream 调度 + runDream(LLM 决策 + 摘要) diff --git a/dsh-mneme/lib/client.js b/dsh-mneme/lib/client.js index 50875a3..fd68888 100644 --- a/dsh-mneme/lib/client.js +++ b/dsh-mneme/lib/client.js @@ -311,7 +311,7 @@ window.__ModuleLoader__.load({ "memory.settings.personaDefaultCard": "默认卡(兜底)", "memory.settings.personaAdd": "添加卡", "memory.settings.personaAddPlaceholder": "输入 agent 身份名(如 nyx),按它寻址注入", - "memory.settings.personaEditingDefault": "正在编辑:默认卡——没有命中任何身份卡的会话用它", + "memory.settings.personaEditingDefault": "正在编辑:默认卡——解析不到身份的会话一定用它;身份解析到了却没建卡(或卡被关掉)的会话,在默认口径下也落到这里,开着「严格人格寻址」则不注入人格", "memory.settings.personaEditingFor": "正在编辑身份卡:", "memory.settings.personaDeleteCard": "删除这张卡", "memory.settings.personaView": "Agent 眼中的你(注入生效文本)", @@ -1029,7 +1029,7 @@ window.__ModuleLoader__.load({ "memory.settings.personaDefaultCard": "Default (fallback)", "memory.settings.personaAdd": "Add card", "memory.settings.personaAddPlaceholder": "Agent identity (e.g. nyx) used to address injection", - "memory.settings.personaEditingDefault": "Editing: default card — used by sessions matching no identity card", + "memory.settings.personaEditingDefault": "Editing: default card — always used by sessions with no resolvable identity; a session whose identity has no card (or whose card is off) also lands here under the default mode, while strict persona addressing injects nothing instead", "memory.settings.personaEditingFor": "Editing identity card:", "memory.settings.personaDeleteCard": "Delete this card", "memory.settings.personaView": "You, as the agent sees you (injected text)", diff --git a/dsh-mneme/test/readme-claims.test.js b/dsh-mneme/test/readme-claims.test.js index d3082a9..492d453 100644 --- a/dsh-mneme/test/readme-claims.test.js +++ b/dsh-mneme/test/readme-claims.test.js @@ -66,3 +66,48 @@ test("隐私节必须写出「巩固与摘要走宿主模型」这个出站事 "English Privacy section should disclose that consolidation uses the host model", ); }); + +// 「模型工具」表是代码的投影,不是愿望清单(AGENTS.md 给 SECURITY.md 定的那条规矩, +// 这张表一直没有)。它已经跑偏两次:#471 加 set_scope_state、#231 加 memory_organize, +// 两次都没人回头改表或改那个数字——因为没人核过。所以这里全部**从代码反推**: +// 工具集从 tools.js 的 name: "…" 取,opt-in 从 hiddenTools 的 `!== true` 判据取 +// (`=== true` 那种是「默认在、用户可关」,不能算进 opt-in,否则默认注册数会算少)。 +test("包 README 的「模型工具」表与 tools.js 注册表一致", () => { + const toolsSrc = readFileSync(path.join(pkgRoot, "src/tools.js"), "utf8"); + const declared = [...toolsSrc.matchAll(/^ {6}name: "((?:memory_|set_scope)[a-z_]*)",/gm)].map((m) => m[1]); + assert.ok(declared.length >= 10, `tools.js 的工具名解析失败(只看到 ${declared.length} 个)——先修这条守卫,别改表`); + const optIn = [...toolsSrc.matchAll(/if \(config\?\.(\w+) !== true\) hiddenTools\.add\("([a-z_]+)"\)/g)] + .map((m) => ({ key: m[1], tool: m[2] })); + const defaultCount = declared.length - optIn.length; + + const heading = pkgReadme.match(/^### 模型工具(([^)]*))$/m); + assert.ok(heading, "包 README 应有「### 模型工具(N 个)」这一节标题(标题格式变了就同步改这条守卫)"); + const bodyStart = pkgReadme.indexOf(heading[0]); + const bodyRest = pkgReadme.slice(bodyStart + heading[0].length); + const nextSection = bodyRest.search(/\r?\n#{2,3} /); + const body = nextSection === -1 ? bodyRest : bodyRest.slice(0, nextSection); + const rows = [...body.matchAll(/^\| `([a-z_]+)` \|/gm)].map((m) => m[1]); + + assert.deepEqual( + [...new Set(declared)].sort(), + [...new Set(rows)].sort(), + "表与注册表必须一一对应:少了是漏文档,多了是文档写了不存在的东西" + ); + assert.equal(rows.length, new Set(rows).size, "表里不许有重复行(重复行会让数量核对失效)"); + // 按**顺序**比数字,不是各自 includes:把「默认注册 11 个;开后 12 个」写成 + // 「默认 12、开后 11」时,两个独立的 includes 都会绿——而那正是这条守卫要防的 + // 第二种错法(第一种是数字与表不符,已被上面的集合比对盖住)。 + const nums = (heading[1].match(/\d+/g) ?? []).map(Number); + const expected = optIn.length ? [defaultCount, declared.length] : [defaultCount]; + assert.deepEqual( + nums, + expected, + `标题里的数字要按序是「默认注册数、全数」= ${expected.join(" 与 ")}(实跑 ${nums.join(" 与 ") || "没有数字"})` + ); + // opt-in 工具那一行必须点名它的开关:读者看到表里有它,得知道默认是不出现的。 + for (const { key, tool } of optIn) { + const row = body.match(new RegExp(`^\\| \`${tool}\` \\|(.*)$`, "m")); + assert.ok(row, `${tool} 必须在表里`); + assert.ok(row[1].includes(key), `${tool} 那行要写明它需要 ${key} 才注册`); + } +});