# dsh-mnemosyne 实现方案 > 把 [`mnemosyne-oss/Pi-mnemosyne`](https://github.com/mnemosyne-oss/Pi-mnemosyne) 的 Mnemosyne 记忆能力移植到 DeepSeek Harness(DSH),以 Profile Bundle(静态 cordis 插件包)形态交付。 ## 1. 背景 Pi-mnemosyne 是 [Mnemosyne](https://github.com/mnemosyne-oss/mnemosyne)(本地优先、SQLite 支持的 AI 记忆层)面向 Pi coding agent 的官方插件。所有记忆逻辑都在 `mnemosyne` CLI(`pip install mnemosyne-memory`)里,插件保持 CLI 优先(一切记忆操作子进程调用 CLI),但**已超出原版"无状态代理"的边界**:会话隔离(§12)需要经 CLI venv 解释器在进程内构造 `Mnemosyne(session_id=...)` 的 Python helper,迁移路由直连 SQLite,写过滤器经 env 桥接。这些都是薄胶水,不重复实现记忆逻辑。 ## 2. Pi-mnemosyne 代码走读 仓库由 host、client、dashboard 资源、测试与文档共同承担四类职责: | 文件 | 职责 | |---|---| | `package.json` | `"pi": { "extensions": ["src/index.ts"], "skills": ["skills"] }` 声明扩展入口与技能目录;peerDeps 引宿主 API 与 typebox | | `src/index.ts` | 默认导出 `(pi: ExtensionAPI)`:调用 `pi.registerTool()` 五次,每次用 typebox `Type.Object` 定义参数,`execute()` 内 `spawn("mnemosyne", [...])` 并返回 `{ content: [{type:'text',text}], details }`;DSH 侧另提供 `mnemosyne_bind` 工作区绑定工具 | | `skills/mnemosyne/SKILL.md` | frontmatter(name/description)+ 使用时机、原生工具说明、最佳实践 | | `tests/index.test.ts` | vitest mock `ExtensionAPI`,断言注册了恰好 6 个预期名字的工具 | | `.github/workflows/ci.yml` | typecheck + test | 核心五个工具及其 CLI 映射;`mnemosyne_bind` 是插件侧工作区绑定工具: | 工具 | CLI 调用 | |---|---| | `mnemosyne_remember(content, source?, importance?)` | `mnemosyne store [source] [importance]` | | `mnemosyne_recall(query, top_k?)` | `mnemosyne recall [top_k]` | | `mnemosyne_forget(id)` | `mnemosyne delete ` | | `mnemosyne_stats()` | `mnemosyne stats` | | `mnemosyne_sleep()` | `mnemosyne sleep` | 错误处理仅两处:spawn 失败提示 `pip install mnemosyne-memory`;非零退出取 stderr。 ## 3. DSH 扩展体系(调研结论) 以下结论来自本机 DSH 安装的权威材料:内置技能 `cordis-plugin-development`、`editing-cordis-compositions`,以及 `@deepseek-ai/dsh-tools` / `dsh-skill` / `dsh-settings` / `dsh-skill-filesystem` 各包 README 和真实第三方插件(`dsh-free-search` 等)源码。 ### 3.1 组合模型 - `dsh --profile ` 启动一个 profile;profile 目录(如 `~/.dsh/profiles/web/`)持有 `package.json`、`cordis.yml`(根,空数组)、用户 patch 层 `cordis.patch.yml`。 - 配置树从空根开始按顺序叠加:**每个 bundle 的 patch 层** → profile 用户 patch → home patch → `--patch`。 - **bundle 的身份由 package.json 的 `"dsh": { "bundle": { "patch": "cordis.patch.yml" } }` 声明**。`dsh plugin --profile web add ` 用 pnpm 安装依赖后,凡解析结果声明了 `dsh.bundle.patch` 的依赖会自动追加进 `dsh.profile.bundles` 层列表;移除时自动退出。 - patch 层是 YAML 数组,典型写法是把插件自身作为一行插入组合树: ```yaml - insert: - id: mnemosyne name: dsh-mnemosyne # 解析为 profile node_modules 里的包入口 config: { ...默认配置 } ``` ### 3.2 插件模块形态(cordis 约定) 入口 ESM 模块使用命名导出: ```js export const name = "mnemosyne"; // 行内标识 export const inject = ["tools"]; // 硬依赖服务;未就绪则插件等待 export const Config = z.object({...}); // schemastery 配置 schema,对应行的 config export function apply(ctx, config) {...} // 注册一切贡献,须放在 effect 内以便随 fiber 清理 ``` 服务访问规则:硬依赖写入 `inject` 导出;可选依赖用 `ctx.get('x')` 判空。未经声明的 `ctx.x` 访问会被 Guard 拒绝。 ### 3.3 工具注册(对应 pi.registerTool) `@deepseek-ai/dsh-tools` 提供 `ctx.tools.register(definition)`,官方惯例包装: ```js ctx.inject(["tools"], (sctx) => { sctx.effect( () => sctx.tools.register(defineTool({ name, description, parameters: { query: { type: "string", required: true, description } }, // JSON-schema 风格 output: { schema: {...}, // 必填:execute 返回值的 JSON schema render: (_args, value) => [{ type: "text", text }], }, async execute(args) { return "..."; }, // 返回值受 output.schema 校验 })), "标签" ); }); ``` 要点:`output` 声明强制存在;`render` 直接返回 block 数组(官方 `web_search` 即如此);注册即进入下一模型步可见;随调用 fiber 自动注销。 ### 3.4 技能系统(对应 pi.skills) `ctx.skills` 注册表 + 多 provider。文件系统 provider 按 rank 扫描:项目 `/.dsh/skills`(100)、`.agents/skills`(200)、自定义目录(300)、`~/.dsh/skills`(400)、`~/.agents/skills`(500)。插件可走更简单的 **runtime 内嵌注册**: ```js const skills = ctx.get("skills"); if (skills) ctx.effect(() => skills.register({ name: "mnemosyne", // 必须 kebab-case description, whenToUse?, content: "", }), "标签"); ``` runtime skill 固定 rank 250:可被项目/用户级同名文件覆盖,又覆盖内置 bundled(600)。省略 invocation 时 model/user 双面可用——与 pi-mnemosyne 的体验一致(随包装、零复制)。 ### 3.5 其余可用面(本方案 v0.1 不用,见 §8) - `ctx.settings.register(ns, schema)` + `./client` 导出 + `dsh.client` 声明 → Web 设置页; - `slots.register` 系列 → 工具卡片、侧栏等 UI; - agent loop / session 事件 → 会话生命周期钩子; - 动态插件(`cordis_define` 等运行时工具)→ 进程内临时插件,不适合分发持久能力,故不采用。 ## 4. 概念映射 | Pi-mnemosyne | dsh-mnemosyne | |---|---| | pi package(`pi.extensions` 入口) | Profile Bundle(`dsh.bundle.patch` 声明 + patch 行) | | `pi.registerTool(...)` | `sctx.tools.register(defineTool({...}))` | | typebox `Type.Object` 参数 | defineTool JSON-schema `parameters` | | `skills/` 目录随包分发 | `ctx.skills.register()` runtime 内嵌技能 | | `.pi/settings.json` packages 安装 | `dsh plugin --profile web add ` | | `/reload` 生效 | 重启 profile 生效 | | vitest + mock ExtensionAPI | node:test + mock ctx(stdlib,免新依赖) | ## 5. 设计决策 - **D1 纯 ESM JavaScript,无构建链**。参照 dsh-free-search / dsh-vision-router 先例,发布即源码;类型靠 JSDoc。跳过 TS/tsc,等项目复杂度上来再引入。 - **D2 继续 CLI 代理,不内嵌存储**。记忆逻辑留在 mnemosyne 本体,插件保持 CLI 优先薄壳,上游升级零成本。v0.4 起为会话隔离与迁移新增两块越界薄胶水(§12):经 venv python 运行的会话级 helper、直连 SQLite 的 default→global 迁移——均不重复实现记忆逻辑。 - **D3 技能内嵌注册而非要求复制文件**。比"复制 SKILL.md 到 ~/.agents/skills"少一步手工操作,且可被项目级同名技能覆盖。 - **D4 硬依赖只声明 `tools`;`skills` 软依赖判空**。最小 headless 组合没有 skills 服务时工具仍可挂载。 - **D5 所有工具输出统一为字符串 + text block 渲染**。CLI stdout 本来就是面向人/模型的文本,不做结构化拆解(YAGNI)。 - **D6 `execFile` + timeout + ENOENT 友好提示**。CLI 缺失时直接给出安装命令,避免裸 ENOENT。 ## 6. 已落地的项目结构 ```text dsh-mnemosyne/ ├── package.json # dsh.bundle 声明;deps: schemastery;peers: dsh-tools ├── cordis.patch.yml # insert 一行:id=mnemosyne, config 默认值 ├── src/index.js # name/inject/Config/apply/SKILL;6 个 defineTool + runMnemosyne ├── test/index.test.js # node:test:参数组装、注册契约、skill、manifest、CLI 失败路径 ├── docs/design.md # 本文档 ├── AGENTS.md / README.md / LICENSE(MIT) / .gitignore ``` 配置项(patch 行 `config:` 可覆盖):`cli`(默认 `mnemosyne`)、`defaultTopK`(5)、`timeoutMs`(20000)。 ## 7. 安装与验证 ```bash dsh plugin --profile web add dsh-mnemosyne # npm 包;开发期可 add <本地路径> # 重启 web profile 后: # 1) 打开 Settings > Mnemosyne,点 Setup 自动安装 CLI(uv tool install mnemosyne-memory) # 或手动:uv tool install mnemosyne-memory # 2) 点 Test connection 验证 store+delete 闭环 # 3) 工具列表出现 mnemosyne_remember / recall / forget / stats / sleep / bind # 4) 技能目录出现 mnemosyne ``` 数据与配置落点:`~/.dsh/mnemosyne/`(`MNEMOSYNE_DATA_DIR` 透传),SQLite 库 `mnemosyne.db` 与 `config.yaml` 均在此目录。 ## 8. 测试策略 测试分三层,均用 `node:test`(stdlib,零额外依赖);当前 `node --test` 共 153 例,包含 workspace identity、dashboard 管理和真实 CLI 集成覆盖。 ### 8.1 单元测试 `test/index.test.js` 与 `test/identity.test.js`(无需 mnemosyne CLI) 1. `storeArgs` / `recallArgs` 位置参数组装(含可选参数缺省); 2. mock ctx 上 `apply()` 恰好注册 6 个预期工具,且每个都有 execute / output.schema / render; 3. skill 注册且名字合法 kebab-case、正文含工具引用; 4. render 输出为合法 text block 数组; 5. CLI 缺失 → 提示 `pip install mnemosyne-memory`;非零退出 → reject; 6. manifest 契约:`dsh.bundle.patch` 指向存在的 patch 文件,patch 含 insert 行。 ### 8.2 集成测试 `test/integration.test.js`(17 例,需真实 `mnemosyne` CLI) 对照 mnemosyne 主仓库 `tests/test_cli_*.py` 的行为契约,对着真实 CLI 跑插件自己的代码路径(`runMnemosyne` + `storeArgs`/`recallArgs` + `apply()` 注册的 `execute()` 闭包)。`resolveCli()` 找不到 `mnemosyne` 时整组自动 skip。 隔离:`before` 钩子把 `process.env.MNEMOSYNE_DATA_DIR` 指向独立 tmpdir、`MNEMOSYNE_NO_EMBEDDINGS=1` 跳过嵌入模型,**绝不触碰用户真实记忆库**;`after` 还原 env 并清理 tmpdir。各用例用唯一 marker 避免相互命中。 覆盖(下表为代表性契约,非穷举): | 用例 | 验证契约 | |---|---| | stats 空库 | `Mnemosyne Stats` + `Total memories: 0` | | remember | `Stored: <16-hex-id>` | | recall 未命中 | `Results for: ` 且无 `ID:` | | recall 命中 | 含 `ID: ` / `Content:` / `Score:` | | forget 有效 id | `Deleted: ` | | forget 缺失 id | reject 含 `Memory not found: `(exit 1 契约) | | sleep | `Consolidation complete` | | 端到端闭环 | remember→recall 命中→forget→recall 确认空 | | execute 闭包 | `apply()` 注册的工具直接驱动真实 CLI(config 钉死绝对路径) | 集成验证(需真实宿主)按 §7 手工执行一次。 ### 8.3 客户端测试 `test/client.test.js`(4 例) 使用 `window.__ModuleLoader__` 与 React mock 验证客户端模块导出、locale 注册、独立 `settings.section` slot、卡片渲染结构和客户端插件契约。 ### 8.4 真实 CLI 行为契约(实测得出) | 命令 | stdout | 退出码 | |---|---|---| | `stats` | `Mnemosyne Stats` + `Total/Working/Episodic/Knowledge triples` 计数行 | 0 | | `store [s] [imp]` | `Stored: <16-hex-id>` | 0 | | `recall [k]` 空 | `Results for: ` 无条目 | 0 | | `recall [k]` 命中 | `ID:` / `Content:` / `Score:` 行 | 0 | | `sleep` | `Consolidation complete: {...}` | 0 | | `delete <缺失id>` | stderr `Error: Memory not found: ` | 1 | | `delete <有效id>` | `Deleted: ` | 0 | ## 9. 发布 版本号 bump → tag `vX.Y.Z` → `npm publish`(public)。关键词沿用 `deepseek-harness` / `dsh-plugin` 便于被检索。 ## 10. 面板、自动安装与 .dsh 数据落盘(v0.2) ### 10.1 数据落盘 `MNEMOSYNE_DATA_DIR` 决定 mnemosyne 的 SQLite 库与 `config.yaml` 位置。插件 `Config.dataDir` 默认 `~/.dsh/mnemosyne`,`buildEnv()` 总是注入 `MNEMOSYNE_DATA_DIR`,因此数据与配置全部落在 `.dsh` 下,不再依赖 `~/.hermes`。 ### 10.2 自动安装 CLI `setupMnemosyne()` 先 `resolveCli("mnemosyne")`;已就绪则确保 `config.yaml` 的面板配置项有默认值后返回。否则 `resolveCli("uv")`,用 `uv tool install mnemosyne-memory` 安装(超时 180s),再探测 CLI 路径并补齐默认配置。面板 Setup 按钮通过 `POST /mnemosyne/setup` 触发。uv 缺失时返回友好错误。 ### 10.3 配置透传 `buildEnv(config)` 负责插件调用 CLI 时的运行时环境,**只注入 `MNEMOSYNE_DATA_DIR`**(始终钉住,它是插件核心契约),外加一个例外:**写过滤器桥接**——上游 store 路径的写入过滤器(`core/filters.py`)只读 `MNEMOSYNE_IGNORE_PATTERNS` / `MNEMOSYNE_WRITE_CLASSIFIER` 环境变量,config.yaml 里的 `ignore_patterns`/`write_classifier` 键从未到达过滤器。因此 `buildEnv` 每次读取 config.yaml,把这两个键(若配置)注入 env(config.yaml > 用户基础 env),让面板 `ignorePatterns` 字段在 `remember()` 时真正过滤噪音(如 `^git `、`^pip install`、`^Traceback`;匹配即静默丢弃,返回 `Stored: None`)。`write_classifier` 不在面板白名单内,用户可手改 config.yaml 启用内置噪音/密钥/结构启发式过滤(`strict` 拒绝 / `warn` 仅记录)。mnemosyne 自身的其余配置由 `dataDir/config.yaml` 管理,文件优先级为 config.yaml > env vars > hardcoded defaults;面板直接读写 config.yaml 中的面板字段。Embedding / LLM / 召回调优 / 工作记忆等上游键**只存在于 config.yaml**,不在 `Config` schema 与 `buildEnv` 里声明,避免形成被 config.yaml 遮蔽的第二条通路;用户全局 env 里已设的 `MNEMOSYNE_*` 会被 config.yaml 未设的键保留。 面板字段对应 config.yaml 的扁平顶层键:`noEmbeddings`→`no_embeddings`、`embeddingModel`→`embedding_model`、`embeddingDim`→`embedding_dim`、`embeddingApiUrl`→`embedding_api_url`、`embeddingApiKey`→`embedding_api_key`、`llmEnabled`→`llm_enabled`、`llmBaseUrl`→`llm_base_url`、`llmApiKey`→`llm_api_key`、`llmModel`→`llm_model`、`llmTimeout`→`llm_timeout`、`polyphonicRecall`→`polyphonic_recall`、`wmMaxItems`→`wm_max_items`、`wmTtlHours`→`wm_ttl_hours`、`autoSleep`→`auto_sleep_enabled`、`sleepThreshold`→`sleep_threshold`、`ignorePatterns`→`ignore_patterns`。`cli`、`defaultTopK`、`timeoutMs`、`dataDir` 及自动记忆六键(`promptSection`/`autoSync`/`autoPrefetch`/`prefetchTopK`/`prefetchMinQueryLen`/`sessionScope`/`recallMode`/`autoWriteScope`)是 DSH 专用设置(运行时目录以 DSH settings 的 `dataDir` 为唯一事实源,config.yaml 不再写 `data_dir`)。 `readMnemosyneConfigYaml()` 解析扁平 YAML 标量并恢复字符串、数字、布尔值和 null 类型;`ensureConfigDefaults()` 在 setup/diagnose 时只为缺失或空值补写 mnemosyne 默认值,不覆盖用户已有值。 ### 10.4 设置面板(client 端) `src/client.js` 以 `window.__ModuleLoader__.load` 格式注册一个 cordis 客户端插件,`apply` 里: 1. `ctx.locale.register("dsh-mnemosyne", {zh, en})` 注册双语字典; 2. `ctx.slots.inject("settings.section", () => ctx.slots.register({name:"settings.section", id:"mnemosyne", order:50, label, locale}, render))` 在 Settings 左侧注册独立入口(仿 dsh-market)。 `MnemosynePanel` 组件(`React.createElement`,无 JSX): - **状态卡**:CLI 就绪/路径、数据目录、记忆计数(解析 `mnemosyne stats` 的 `Total memories:`); - **Setup 按钮**:`POST /mnemosyne/setup` 自动安装 CLI,并补齐 config.yaml 默认值; - **Test 按钮**:`POST /mnemosyne/test` 跑 store+delete 探针,成功后立即调用 diagnose 刷新 stats; - **Refresh 按钮**:`GET /mnemosyne/diagnose` 刷新状态; - **配置卡片**:状态卡和插件卡默认展开,其余配置卡默认折叠;每个字段采用左侧名称/问号 tooltip、右侧输入控件的单行布局;空值字段用默认值 placeholder; - **恢复默认配置**:`DELETE /mnemosyne/config` 将面板管理的配置写回 mnemosyne 默认值并执行 config reload; - **底部提醒**:提示用户可直接编辑 `~/.dsh/mnemosyne/config.yaml` 修改更多参数。 配置编辑由 DSH settings 与扁平 config.yaml 共同承担。config.yaml 字段通过 HTTP 路由读写,保存后自动执行 `mnemosyne config reload`;大部分配置无需重启,`vec_type` 等启动时确定的配置例外。 `package.json` 声明 `dsh.client: { inject: [...client-ui 包], platform: "web" }` 与 `"./client"` 导出。 ### 10.5 Host HTTP 路由 `apply` 软依赖 `ctx.inject(["webServer"])`,注册 HTTP 路由供面板 `fetch` 调用: | path | method | 行为 | |---|---|---| | `/mnemosyne/diagnose` | GET | 探测 CLI + 确保 dataDir + 跑 stats,返回结构化报告 | | `/mnemosyne/setup` | POST | 检测/安装 CLI(uv tool install) | | `/mnemosyne/test` | POST | store 探针记忆 → delete,验证闭环 | | `/mnemosyne/config` | GET | 读取扁平 dataDir/config.yaml,**只返回面板管理字段的允许列表**(允许列表之外的键一律不下发),再合并 mnemosyne 默认值;密钥类值(`embedding_api_key`、`llm_api_key`、`llm_fallback_api_key`、`conflict_llm_api_key`、`sync_key` 等)以掩码 `***` 返回,真实值不出服务端 | | `/mnemosyne/config` | POST | 写入面板管理的顶层 config.yaml 键,并执行 `mnemosyne config reload`(收到掩码 `***` 的密钥键视为"未修改",跳过写入,防占位符覆盖已存密钥;空串则是显式清除) | | `/mnemosyne/config` | DELETE | 将面板管理的顶层键恢复为 mnemosyne 默认值,并执行 config reload | | `/mnemosyne/migrate-default-session` | POST | 把历史 `default` 会话记忆翻转为 `global` 作用域(venv python 直连 SQLite,事务 + 回滚,内容不变) | | `/mnemosyne/migrate-session-scopes-to-default` | POST | 把 `dsh_*` session 行合并回共享 `default` 命名空间;这是显式不可逆操作,会丢失原会话归属 | ### 10.6 会话收尾自动 sleep `turn/end` 只把自动同步和周期 consolidation 排入按 session 串行的后台队列,不阻塞事件生产;每 10 个 durable turn 检查一次,`config.yaml` 的 `auto_sleep_enabled` 为 true 且工作记忆条目数 ≥ `sleep_threshold`(默认 50)时执行当前 session 的 `mnemosyne sleep`。阈值键被清空(`""`)或写成 0 / 负数时回退到默认 50,绝不按 `Number("")===0` 解释成「每轮必睡」。`session/disposed` 只在该会话本次生命周期内确实写入过自动记忆(`shouldRunSessionEndSleep`,WeakSet 按 session 对象跟踪)时才在排空同步后强制执行收尾 consolidation;空闲 session 不会触发可能调用 LLM 的 sleep。两个迁移路由(default→global、scoped→default)与工具写入共用同一把按 dataDir+bank 串行的 `withMemoryLock`,不会与 remember/forget/sleep/auto-sync 并发改写同一 SQLite 库。 ## 11. 自动记忆增强(v0.3) 对标 mnemosyne 主仓库当前 canonical Hermes 集成的三层自动化能力(system prompt 注入、pre-turn prefetch、post-turn sync),在保持 DSH CLI/venv 适配边界的前提下,利用 DSH 的 `agent/pre-step` waterfall 与 `session/event` / `session/disposed` 生命周期实现等价功能。**三项功能默认开启**,显式设置为 false 的既有用户配置仍优先。 ### 11.1 配置项 | Config 字段 | 默认 | 作用 | |---|---|---| | `promptSection` | `true` | 在 system prompt 注入 `# Mnemosyne Memory` 声明段 | | `autoSync` | `true` | 每轮对话后自动将**真实 user 消息**存入 Mnemosyne(默认不同步 assistant 输出;注入型上下文——`plugin` / `agent-instructions` / `skill-catalog`——永不入库) | | `syncTurnUserLimit` | `500` | user 自动同步字符上限;`0` 表示不截断,对齐 `MNEMOSYNE_SYNC_TURN_USER_LIMIT` 语义 | | `syncTurnAssistantLimit` | `800` | assistant 自动同步字符上限;`0` 表示不截断,对齐 `MNEMOSYNE_SYNC_TURN_ASSISTANT_LIMIT` 语义 | | `autoPrefetch` | `true` | 每个模型步骤前自动 recall、过滤并注入不可信记忆上下文 | | `prefetchTopK` | `5` | 自动召回注入时最多保留的记忆条数 | | `prefetchMinQueryLen` | `1` | DSH 兼容阈值;Hermes 的 trivial prompt gate 由宿主负责 | | `recallMode` | `session`(存量兼容) | 召回边界:`default`、当前 `session`,或已绑定工作区的 `workspace`;workspace 未绑定时不静默降级 | | `autoWriteScope` | `session`(存量兼容) | 自动记忆写入层级:`default`、当前 `session` 或已绑定工作区的 `workspace` | | `sessionScope` | deprecated | 无显式新 scope 时保留旧语义:`true` 为 session,`false` 为 legacy default | ### 11.2 systemPrompt.section — 静态声明段 当 `promptSection` 为 true 时,通过 `ctx.get("systemPrompt").section()` 注册一个 order=95 的 prompt 段(在 tool guidance 100-199 之前),内容为 `# Mnemosyne Memory` 头部声明,告诉模型记忆工具可用及其使用方式。软依赖 `ctx.get("systemPrompt")`,不影响 inject 声明。 对应 hermes provider 的 `system_prompt_block()`。 ### 11.3 session/event — 自动对话存储(sync_turn) 当 `autoSync` 为 true 时,扩展已有的 `session/event` 监听器,除了 `turn/end` 的 auto-sleep 逻辑,还处理**真实 user 消息**: - 只处理 `source.kind === 'user'` 的 `user/message` 事件(每轮一次,不做逐 step 同步) - 注入型上下文一律跳过:`plugin`(含本插件自己的 prefetch 注入)、`agent-instructions`、`skill-catalog`——与 `extractLastUserText()` 的注入来源名单保持一致,防止 system-reminder / AGENTS.md / 技能目录进入记忆并形成召回反馈循环 - `user/message` 采集完整 text content;在 `turn/end` 对配置做当轮快照后,按 `syncTurnUserLimit` 截断并执行 `mnemosyne store conversation 0.5` - assistant 消息默认不自动存储;`sync_roles` 包含 assistant 时,同样按当轮 `syncTurnAssistantLimit` 快照截断后写入 - 两个 limit 均使用 Unicode code point 计数;正数保留前缀,`0` 不截断,与 canonical Hermes 的环境变量语义一致。完整保存模式不自动分块或摘要 `extractMessageText()` 从消息 content blocks(`[{type:"text",text}]` 数组)中提取纯文本。失败静默跳过。 对应 hermes provider 的 `sync_turn()`。区别:hermes 是 in-process 直接调用 `beam.remember()`,dsh-mnemosyne 是 out-of-process spawn `mnemosyne store` 子进程。 ### 11.4 agent/pre-step — 自动召回注入(prefetch) 当 `autoPrefetch` 为 true 时,通过 `ctx.on("agent/pre-step", ...)` 注册一个 prepend 监听器(`inject: ["agents"]`)。每个模型步骤前: 1. 调 `next()` 拿到下游决策(`{kind:'enter', messages}` 或 `{kind:'reject'}`) 2. 从 `decision.messages` 中提取最后一条 user 消息文本作为 query 3. 短消息(< `prefetchMinQueryLen`)跳过,避免对 "hi"/"ok" 浪费 CLI 调用 4. 同一 turn 内已 prefetch 过的相同 query 跳过(`prefetchedQueries` Set 去重) 5. `mnemosyne recall ` → 检查结果是否含 `ID:` 行(命中判断) 6. 命中则用 `formatPrefetchContext()` 格式化为 `## Mnemosyne Context` 文本块 7. 返回 `{kind:'enter', messages: [...decision.messages, injectedMsg]}`,注入消息带 `source: {kind:'plugin', plugin:'mnemosyne', ...}` 8. recall 失败或无命中时原样返回 decision,不干扰会话 对应 hermes provider 的 `prefetch()`。`dsh-time-context` 插件使用相同模式注入时间上下文。 ### 11.5 inject 变更 `export const inject` 使用 `["tools", "agents", "sessions"]`。`agents` 服务提供 `ctx.on("agent/pre-step", ...)` 事件注册能力,`sessions` 用于 root session 与 workspace cwd 解析;在 web profile 下二者是核心服务。`systemPrompt` 通过软依赖 `ctx.get("systemPrompt")` 访问,不改 inject。 ### 11.6 面板 新增 `groupAuto`(自动记忆)配置组,包含 prompt、sync、prefetch 和 scope 字段:`promptSection`、`autoSync`、`syncTurnUserLimit`、`syncTurnAssistantLimit`、`autoPrefetch`、`recallMode`、`autoWriteScope`、`prefetchTopK`、`prefetchMinQueryLen`。这些字段是 DSH 侧配置(不写入 config.yaml,不走 buildEnv),由 DSH settings 管理并在 turn 边界快照;旧 `sessionScope` 作为 deprecated 兼容别名。workspace 模式要求显式绑定 `.mnemosyne-id`。 ### 11.7 测试 自动记忆与会话隔离测试覆盖: - 自动记忆配置默认值(6 例):验证 Hermes 风格默认开启、systemPrompt.section 条件注册、agent/pre-step 条件注册 - `extractMessageText`(5 例):从 content blocks 数组提取文本 - `extractLastUserText`(3 例):从 messages 数组提取最后一条 user 消息 - `formatPrefetchContext`(3 例):格式化 recall 输出为 prompt 注入文本 - sync turn limit:验证 Hermes 500/800 默认值、`0` 不截断、Unicode 安全截断及真实 CLI 尾部内容写入 - 其余参数、作用域、迁移和 CLI 契约用例保持通过 ## 12. 会话隔离(sessionScope,v0.4) CLI 的 store/recall 没有会话参数,workspace/session 路径通过 CLI venv helper 显式传入 namespace。新安装可选择 workspace/workspace;存量配置通过 deprecated `sessionScope` 保持原 session/default 语义,用户显式切换后再启用 workspace。升级时 legacy default 与旧 session 行均不自动改写,迁移必须由用户在管理区确认。 ### 12.1 session_id 派生与迁移 - `deriveSessionSid()`:形如 `session-` 的持久 id 原样加 `dsh_` 前缀。`session-` 计数器形 id 在 DSH 持久化恢复后会保留原 id(`header.createdAt` 同样持久化),因此 sid 派生自 **session header 的 `createdAt`**(`dsh__`):恢复的会话跨 DSH 重启保持同一 sid,记忆不丢;同 id 的新建会话 createdAt 必不同,不会撞名。(不能用每 boot 随机 uuid——会让恢复会话的旧记忆变成孤儿。) - `findRootSession()`:沿 `header.parentSession` 链走到根会话——子代理归入其根会话,委托工作共享主会话记忆(有意设计,非缺陷)。 - 会话对象经 WeakMap 绑定 sid,不持有强引用;fiber 销毁后 GC,下次事件重新绑定。 - **迁移(default → global)**:历史 `default` 会话记忆在隔离下不可见(recall 只命中 `session_id = ? OR scope = 'global'`)。记忆浏览器管理区调用 `POST /mnemosyne/migrate-default-session`,经 venv python 直连 SQLite,把 `working_memory`/`episodic_memory` 中 `session_id='default'` 且非 global 的行翻转为 `scope='global'`(事务 + 回滚,内容不变)。triples 表无 session_id/scope,天然共享,无需迁移。 - **反向迁移(dsh session → default)**:关闭 `sessionScope` 前可在记忆浏览器管理区调用 `POST /mnemosyne/migrate-session-scopes-to-default`,把 `dsh_*` 的 session 行合并到 legacy `default`。该动作会丢失原会话归属,必须显式确认。 - **工作区管理**:记忆浏览器管理区调用 `/mnemosyne/workspaces` 查询绑定与候选,使用 `/mnemosyne/workspaces/bind` 或 `/adopt` 写入 marker/index;工作区迁移调用 `/migrate`,先 dry-run,再执行并创建 `backups/` 快照,支持使用该快照 revert。 ### 12.2 venv python 直驱 helper CLI 无会话参数,因此 `SESSION_HELPER`(`mnemosyne_session_helper.py`,写入 dataDir)通过 CLI shebang 解析出的 python 在进程内构造 `Mnemosyne(session_id=..., bank=...)`,实现 store/recall/delete/sleep: - store 接受显式 `session` / `global` scope;recall 强制 `_cross_session=False`,不让 config.yaml 绕过隔离;bank 从 `MNEMOSYNE_BANK` 继承,与 CLI 保持同一数据库 - recall 输出格式与 CLI 严格一致(`Results for:` / `ID:` / `Content:` / `Score:` / `[entity match]`),`formatPrefetchContext` 与工具文本解析零改动 - argv 直传内容(execFile 无 shell,仅 NUL/长度边界;auto-sync 已截断 500/800) ### 12.3 整合路径 - `mnemosyne_sleep` 在 session/workspace scope 下默认只整合当前目标 namespace;需要全库整合时使用显式的面板/CLI 入口。自动 sleep 只整合触发它的当前 namespace,避免后台任务改变其他 profile 的活跃上下文。 - `mnemosyne_stats` 与面板统计为全库计数,不按会话区分(工具语义如此,属预期)。 ### 12.4 依赖与边界 - 会话路径依赖 CLI shebang 解析出的 python;绝对解释器与 `#!/usr/bin/env python3` 形式都支持。 - **显式 scope 一律走 helper,只有 legacy `default` 池走裸 CLI**:`mnemosyne store [source] [importance]` 没有 scope 参数,scope 恒为 `MNEMOSYNE_DEFAULT_SCOPE` / `"session"`,因此 `mnemosyne_remember` 的 `scope=global` 与 `scope=workspace` 必须经 helper 直传 `Mnemosyne.remember(scope=...)`,否则会被静默降级成 session 行。global 行落在 `session_id='default'` 且 `scope='global'`,与 §12.4 的 default→global 迁移产物同形。 - 存量升级默认保留 legacy `sessionScope` 语义;`default` 记忆不会自动迁移。workspace 共享需显式绑定 marker 并切换 `recallMode`/`autoWriteScope`,旧配置不会被静默改写。 - **global 是所有会话的共享命名空间,可读可写**:任何会话都能 recall 到 global 行,也能 delete 它们(engine 的 forget SQL 是 `session_id = ? OR scope = 'global'`)。这是上游语义,插件不额外做删除保护;需要只读公共记忆时别用 global。 - **`cross_session` 被会话 helper 强制关闭**:上游 config.yaml 的该键不会扩大 sessionScope 的 recall 范围。 ### 12.5 安全:config 密钥掩码 `GET /mnemosyne/config` **只返回面板管理字段的允许列表**(允许列表之外的 config.yaml 键一律不下发,包括 `llm_fallback_api_key`、`conflict_llm_api_key`、`sync_key` 等未列入面板的上游密钥),密钥值统一返回固定掩码 `***`(`MASKED_SECRET`),真实值只存在于服务端 config.yaml,杜绝 GET 回显明文密钥(写路径有 sameOrigin,GET 只有 Sec-Fetch-Site 兜底)。`POST /mnemosyne/config` 收到掩码值时跳过该键(视为"未修改"),面板整表回写不会用占位符覆盖已存密钥;显式传空串才是清除。 ## 13. 后续可选增强 - 去 CLI 化直连 SQLite — 与上游架构冲突,除非上游 API 变动否则不考虑。