# dsh-mem **Repository Path**: ffeng86/dsh-mem ## Basic Information - **Project Name**: dsh-mem - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-20 - **Last Updated**: 2026-11-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # avantf-mem 用 TypeScript 编写的 DSH 原生**长期记忆**插件。它既是 DeepSeek Harness 的**原生 Cordis 插件**,同时保留以 **MCP server** 运行的能力。 > 文档知识库已拆到独立插件 **`dsh-knowledges`**(`knowledge.db`、docs、`kb_*` 工具、知识页与文档转换都在那边)。本仓只负责记忆:facts / journal / notes、检索、时间线、维护。 - **记忆** — 结构化事实 + SPO 三元组 + 实体;混合检索(语义嵌入 + FTS5 + 实体 Jaccard);生命周期管理(强度衰减 / 遗忘 / 归档 / 矛盾 / 去重)。不做用户隔离:agent 上下文共用单一存储。 - **内部可插拔检索** — `SemanticBackend` / `VectorStore` 可在 `retrieval-core` 内部替换(注册表 + 配置 + 优雅降级 + 自动升级),无需改动业务流程。 - **方案 A** — 检索在本地完成(onnx `bge-base-zh-v1.5`);答案生成在外部(DSH / agent 模型)。 2026-10-30 业务审查修复说明及复测见 [修复报告](docs/FIX-2026-10-30-REPORT.md):包含事务隔离、导出/恢复安全、实际量化加载、范围召回、取消与分片索引。升级会添加数据库 v20 索引;本地 BGE 表示新增实现世代,旧向量按 `semantic.auto_migrate` 策略一次性重算(关闭时只告警并隔离旧坐标),正文不删除。导出新写 v7、继续读取 v1–v6;运行中的 DSH 需要重启宿主才使用新实现。 完整架构与路线图见 [DESIGN.md](DESIGN.md),DSH 分步安装手册见 [docs/INSTALL.md](docs/INSTALL.md),向量后端选型见 [docs/VECTOR_STORES.md](docs/VECTOR_STORES.md),预装设施现状(底座 `@avantf/dsh-plugin-base` 的环境初始化框架接管:item 清单、受管根、legacy 降级与迁移配方)见 [docs/PROVISIONING.md](docs/PROVISIONING.md)。 > **硬依赖底座**:本插件强依赖 `@avantf/dsh-plugin-base`,这一点无法回避。 > - **运行期**:它是插件的普通 `dependencies`(宽区间 `>=0.3.0 <1.0.0`),装插件会自动带上,用户不需要单独装; > 但底座缺失或过旧时插件只会**降级挂载**——启动期资源初始化、宿主兼容门禁、prompt 文件层等能力不可用。 > 本产物 bake 的是 **interface v6**(base 0.7.0):`runtime-deps` 共享根自愈(`semantic.self_heal`) > 与它的会话提示(`res/runtime-deps.yaml` 的 `audit`)都走这一代的 `createRuntimeDeps` helper。更旧的底座 > (v5 及以前)会被接口门禁判为 `incompatible`:插件不用底座的共享能力、走 legacy 路径,**照常挂载** > (自愈项因此不存在);运行时仍回退到自己的 optional 解析。 > - **构建/发布期**:必须先有一份 base checkout 并构建它(`pnpm build:dsh` 会自动安装/构建/链接 base), > 否则插件无法 link、typecheck、build 或发布。发布顺序固定为 **base → mem**。 > 详见 [docs/RELEASING.md](docs/RELEASING.md)。 ## 数据目录 ``` ~/.avantf/ # 默认 data_home ├─ configs/ │ ├─ common.yaml # 公共配置(semantic / vectorStore / retriever / lifecycle) │ └─ memory.yaml # 记忆专属覆盖(缺失 = 不覆盖) ├─ prompts/ │ └─ mem-memory-usage.md # 用法提示(缺失或为空时写入内置默认) ├─ memory/ │ └─ memory.db # 记忆存储 + 记忆读模型 └─ backups/ # `mem_admin export` 的默认落点 ``` `~/.avantf/knowledge/**`(`knowledge.db`、受管 `.md`)属于 `dsh-knowledges`:本插件既不读也不删,升级时原样留在盘上。 ## 包结构 | 目录 | 包名 | 用途 | |---|---|---| | `packages/retrieval-core` | `@avantf/mem-retrieval` | 可插拔检索底座:接口 + 注册表 + 配置解析 + 降级 + 融合 | | `packages/core` | `@avantf/mem` | 引擎:记忆存储、检索编排、生命周期、矛盾检测、导入导出 | | `packages/contract` | `@avantf/mem-contract` | 工具 / 检索 / 管理契约的唯一来源(zod) | | `packages/provision` | `@avantf/mem-provision` | 依赖获取唯一入口:启动预热(嵌入模型 + 分词器)的 artifact 注册表 + 受管目录 + 镜像下载 | | `packages/plugin` | `@avantf/dsh-mem` | DSH 原生 Cordis 插件(宿主 service + tools + RPC + `conversation.view` 客户端标签页「记忆」) | | `packages/cli` | `@avantf/mem-cli` | CLI(memory / contrad / maintenance) | | `packages/mcp` | `@avantf/mem-mcp` | 可选的 MCP server 入口(stdin/stdout,复用契约) | > **底座依赖(强依赖)**:启动期环境初始化与宿主兼容门禁都在外部包 `@avantf/dsh-plugin-base`(本仓不含它的源码)。 > 本仓把底座声明为插件的**普通运行期依赖**(`dependencies` 里的宽区间 `>=0.3.0 <1.0.0`,装插件即自动带上; > 另在 `devDependencies` 里声明同一条,好让本地 `pnpm install` 有一份可解析的底座):**不内联、也不按 > specifier import**——插件唯一的静态引用是内联的零依赖 `bootstrap`(`packages/plugin/src/envinit-bootstrap.js`), > 它按 `createRequire(...).resolve('@avantf/dsh-plugin-base/package.json')` 从插件自己的依赖树解析底座、动态 > `import()` 并校验版本,拿不到就一条 `envinit: WARNING` 后**照常降级挂载**。运行时从底座取用门禁规则/探针/ > 复查、envinit provisioner 与 prompt 文件层 `PromptFiles`,所以修这些共享代码只需发一次底座。 > 构建时必须先有 base checkout 并构建它:`scripts/link-envinit.mjs`(`pnpm build:dsh` 会自动跑)从**已链接的 > 底座 checkout** vendor `bootstrap`(`pnpm link:base` 把 `../base` 或 `$AVANTF_BASE_DIR` 链进插件依赖树; > 就地联调另一份底座时用 `DSH_ENVINIT=` 显式指定)。**没有 base checkout 就无法构建/发布本插件**; > 发布顺序固定为 **base 先、mem 后**。 > `zod` 由本仓 `pnpm-workspace.yaml` 的 catalog 统一成一份(`zod: 4.4.3`,跟随已安装 dsh 的版本),底座的 > `zod` peer 保持 `>=4.4.3 <5`;插件自己也把 `zod` 声明成 **required peer**(同一条 `>=4.4.3 <5`, > `devDependencies` 用 `catalog:` 供本仓构建与测试解析):运行期用**宿主那一份**,免得第二份 zod 造成 schema > 类型身份分叉(DESIGN §20.11)。改底座里的共享代码后,本仓的完整门禁要重跑(`pnpm release:check`,含 > `build:dsh` 与 mount smoke);判据是"这条知识能不能靠一次底座发布修好"——能就在运行时从底座取用,不能 > (只是两三行照抄宿主约定的字面量)可以留在插件里,但要写明"改它需要发插件"。 > 详见 [docs/RELEASING.md](docs/RELEASING.md)。 ## 与底座仓一起 checkout(repo 布局) 本仓是独立仓库,**不含**底座源码;底座是**强依赖**的外部 checkout(构建/发布必须有它)。推荐的 repo 布局是: ``` / ├── base/ # @avantf/dsh-plugin-base 的 clone(仓库根 = 包根) └── mem/ # 本仓库(仓库根 = mem 仓根) ``` ```bash cd /mem pnpm install --frozen-lockfile # 首次:本仓 workspace 依赖 pnpm build:dsh # 一条命令:准备 base(install/build/link)→ 引擎 → 插件 → typecheck → mount smoke pnpm run pack # 打包 → release/avantf-dsh-mem-.tgz pnpm release:check # 发版门禁(可选;含 registry 探测与 old-dsh) ``` `build:dsh` 负责把依赖准备好再编译: - base checkout 没有 `node_modules` 时,`build-base` 用 `pnpm install --frozen-lockfile --ignore-scripts` 装好并 `pnpm build`; - `link-base` 把 `../base`(或 `$AVANTF_BASE_DIR`)链进插件依赖树,`link-envinit` 从它 vendor `bootstrap`; - 本仓 `packages/plugin/node_modules` 缺失时,`build-plugin` 会先 `pnpm install`; - 然后构建引擎包、构建插件(tsc + tsdown)、跑引擎与插件的 typecheck、`test:dsh` 与 mount smoke。 `pnpm build` / `pnpm typecheck` 仍保留为低级命令(只做引擎包),日常编译用 `build:dsh` 一条即可。 打包命令与 base 仓一致:`pnpm run pack`(`pack:plugin` 是同一脚本的别名;产物统一在 `/release/`)。`link:base` / `build:base` 仍可单独跑,用于排错或只重建底座。 `link:base` 只替换 `packages/plugin/node_modules/@avantf/dsh-plugin-base` 这一个插槽,**不改 manifest**: 发布出去的依赖仍是 registry 区间 `>=0.3.0 <1.0.0`。底座不在 `pnpm-workspace.yaml` 的 `packages:` 里—— 它不是本仓 workspace 的项目,把 `workspace:` 写进可发布 manifest 正是发布门禁禁止的。 ## 已实现里程碑 | # | 状态 | |---|---| | M0 项目骨架 + 配置分层 + `~/.avantf` 布局 + 记忆 schema | ✅ | | M1 HRR 代数 + 确定性原子 + 实体/三元组 | ✅ | | M2 记忆混合检索 + 35 查询一致性测试台 | ✅(测试配置下的冻结基线 MRR 0.971,R@k 0.957) | | M3 本地 ONNX BGE(惰性降级) | ✅ | | M4 5 种向量库注册表接口 + 自动升级 | ✅(local_numpy 与 hnswlib 为具体实现;`auto` 达阈值自动升级;faiss/pgvector/qdrant 是会显式告警的适配器接口) | | M5 生命周期 + 矛盾检测 + 去重 | ✅ | | M6 可插拔注册表定型 + 测试 | ✅ | | M7 DSH 宿主插件(service + tools + RPC) | ✅(可构建;运行需要 DSH profile) | | M8 客户端记忆页(`conversation.view` 标签页「记忆」) | ✅(可构建;渲染需要 web bundle) | | M9–M10 文档知识库(存储 / 交叉检索 / 受管副本 / 转换 / 知识页) | ➡️ 已迁到独立插件 **`dsh-knowledges`**(本仓不再包含) | | M11 完整 CLI 子命令 | ✅(**需 repo checkout 运行**:`@avantf/mem-cli` 是 `private`,不进可发布集合——见下方"另外 5 个包") | | M12 MCP 入口(stdio,复用契约) | ✅(`tools/list` 与 3 个模型工具的 `tools/call` 都在 CI 里真实派发过;**需 repo checkout 运行**:`@avantf/mem-mcp` 是 `private`,不进可发布集合) | | M13 一致性回归 | ✅ | | M13b 发布(1.0 待 tag) | ⏳ 见 [docs/RELEASING.md](docs/RELEASING.md):`pnpm release:check` + 打 tag,尚未切版本文件 | > **验证边界(重要)**:M7/M8 在本仓库内只经过**编译与类型检查**(CI 跑到的那部分), > 而它们的**运行时**——插件真的能 mount 进 DSH、记忆页真的能渲染——只能在有 harness 的机器上验证, > 即 `pnpm build:dsh`(内含 typecheck、`test:dsh` 与 mount smoke)。这两步是**本地门禁**,CI 没有 > harness(其传递依赖并非全部发布到 npm)。因此:**打 tag 必须在跑过 `pnpm release:check` 的机器上做**, > 发布清单与"支持的宿主窗口"见 [docs/RELEASING.md](docs/RELEASING.md)。 ## 模型引导(镜像/自动下载) 当 DSH 插件接入后,启动时**异步**加载语义模型(不阻塞 `apply`)。**模型的下载落点与来源由底座框架决定,不再由终端用户的 `~/.avantf/configs/common.yaml` 决定**: - `mem:model` 是框架的 `model-cache` item,用 **`flat` 布局**落到 `/models//`——正是 `@huggingface/transformers` 在其 `cacheDir` 下读的形状。默认仓库是 `Xenova/bge-base-zh-v1.5`(768 维,首次下载约 **389 MB**)。文件列表只对**默认仓库**写死运行时真正会取的四个(`config.json` / `tokenizer.json` / `tokenizer_config.json` / `onnx/model.onnx`);改过 `semantic.local_model` 的仓库**不写列表**,交给框架按仓库自身的文件清单装,不会因为固定的四件套与仓库不符而 `failed`。运行时因此复用框架装好的那一份,不会二次下载;`buildRuntime` 在框架接管模型根时**延迟**后端构造时的预热,预热只从 `mem:model` 到达终态的回调开始,不会和框架的安装抢同一份文件。 - 缓存目录 = 框架的族根 `/models`(`~/.avantf/env/models`,由 `managedRoots` 设成内建默认);`configs/common.yaml` 里的 `semantic.cache_dir` 会被**忽略并告警**。运维用 `AVANTF_MEM_MODEL_CACHE` 把缓存指到别处时,运行时读那一份,`mem:model` **不再声明**——否则框架会在族根再装一份没人读的完整副本。 - 镜像默认 `https://hf-mirror.com`,`configs/common.yaml` 里的 `semantic.mirror` 同样被忽略;运维只能用环境变量逃生口(见下)。 - 模型 item 拿不到时按**降级**处理:`semantic.auto_download` 打开就由运行时自己取;关闭时 item 仍会声明,但框架判为 `skipped (policy/download-disabled)`、运行时也只读磁盘,检索退回 FTS+entity,**绝不拒载**。 ```yaml # ~/.avantf/configs/common.yaml semantic: local_model: Xenova/bge-large-zh-v1.5 # ⚠️ transformers.js 需要 ONNX 仓库(1024 维) dim: 1024 # 必须与模型一致;改它会作废旧向量(见下) dtype: q8 # 权重量化(fp32 更慢更大,q8 是默认) auto_download: true # 启动时自动下载(false 则只在已缓存时加载) rerank: # 标准 RAG 的第二段:召回 → 重排 → topK enabled: true # 关闭时回到"融合序 + min_score"的旧路径 backend: local local_model: Xenova/bge-reranker-base # 300 判据实测:质量与 v2-m3 持平、p50 343ms vs 989ms dtype: q8 # 283 MB candidates: 5 # 重排的候选数(延迟 ≈ candidates × 每条 token 数) timeout_ms: 8000 # 超时即回退融合序,不会让查询失败 refuse_below: -5 # 重排分低于此值就回空页(校准值,换模型要重测;null=不拒答) auto_download: true # 默认自动下载(与 enabled 一起默认开;关掉则需先把模型放进缓存) self_heal: # 默认关闭:打开后允许启动期安装缺失的 npm 包 enabled: false native_self_heal: # 默认关闭:nodejieba / hnswlib-node(node-gyp,独立共享根) enabled: false # 上面两个开关是这个文件里**唯一**的自愈相关配置:装哪些包、从哪个 registry 装、验证什么、 # 以及「哪个依赖缺失时对会话说什么」,全部写死在插件自带的 res/runtime-deps.yaml 里(随包发布, # 不从用户配置读取)。升级插件即升级这份声明,profile 不会与代码脱节。 ``` > **换嵌入模型的迁移步骤**(`semantic.local_model` / `dim` 任一改变):先 `VACUUM INTO` 备份数据库 → > 改配置 → 重启(启动时按批重嵌入,`migrateVectors`;`semantic.auto_migrate: false` 则只告警)→ > 用 `mem_admin vectors_diagnose` 确认 `stale_*` 归零。旧维度的向量在这种切换后**不可用**,语义腿在 > 迁移完成前只覆盖新写入的行。 > > **延迟预算**:重排的耗时≈`candidates × 每条候选的 token 数`(本机 568M q8 模型约 1 ms/token-row)。 > 实测 `candidates=5` + 候选截断到 320 字 ≈ 0.9 s/查询;`candidates=10` + 不截断 ≈ 5 s/查询。 > > **ONNX 注意**:`@huggingface/transformers`(transformers.js)需要 **ONNX 模型仓库**。 > `Xenova/bge-base-zh-v1.5`(或 `onnx-community/bge-base-zh-v1.5`)是 ONNX 版; > `BAAI/bge-base-zh-v1.5` 是 PyTorch 版,**不能直接用于 transformers.js**(会因找不到 ONNX `model.json` 而降级)。 > 配置默认值即 ONNX 仓库 `Xenova/bge-base-zh-v1.5`(开箱即有语义检索),加载成功后产出真实 **768 维**向量,fp32 > 权重首次下载约 **389 MB**、**首次冷启(下载 + 加载)实测约 16.6 s**;权重落到族根后离线可用(本机二次从缓存 > 加载实测约 0.9 s)。 > **镜像不一定可达**:`hf-mirror.com` 在部分网络下 80/443 均连接超时(ICMP 也不通),此时语义路径会降级;把镜像环境变量换成可达的 HF 镜像即可(例如 `https://aifasthub.com`,实测可用——它 302 到 `us.aws.cdn.hf.co`)。 镜像与落点的优先级(只剩环境变量这一层,`configs/common.yaml` 不再是真相来源): - 镜像:`AVANTF_MEM_MODEL_MIRROR`(或 `HF_ENDPOINT`)> 默认 `https://hf-mirror.com`;环境层把它写进 `semantic.mirror`,因而同时喂给 `mem:model` 的 `spec.endpoint` 与适配器的 `remoteHost`。 - 缓存目录:`AVANTF_MEM_MODEL_CACHE` > 框架族根 `~/.avantf/env/models`(无框架的 CLI/MCP 与降级路径为 `~/.avantf/env/models`);环境层同样写进 `semantic.cache_dir`,设了它就不再声明 `mem:model`。 - 下载总闸:`AVANTF_ENVINIT_AUTO_DOWNLOAD=0`(底座级)或 `AVANTF_MEM_AUTO_DOWNLOAD=0`(本项目)——任一为 `0` 都会经环境层落进 `semantic.auto_download` **与 `rerank.auto_download`**,框架与运行时**同时**停手。 - 重排开关:`AVANTF_MEM_RERANK_ENABLED=0` 关闭重排(**默认开启**,见 `common.yaml` 的 `rerank.enabled`),不需要改配置文件;`1` / `true` 反向打开。 - 模型未就绪时检索**自动降级**到 FTS + 实体 Jaccard(`isAvailable()` = false);下载完成后自动升级到 `0.55 / 0.30 / 0.15` 三路融合。**降级不是终态**:启动预热失败后,后续检索/索引会自动重试(同一时刻只跑一次,最短间隔 30s),无需重启进程;`vectors_fix` 则会当场等待一次完整尝试。`auto_download: false` 时不重试(本地缺失是确定性的)。CLI 启动时同步预热;插件与 MCP 异步预热(MCP 先应答 `initialize`,再后台加载模型)。 - `@huggingface/transformers` 已声明为 `@avantf/mem-retrieval` 的 **optionalDependency**,由适配器动态加载;首次运行时从镜像下载 BGE 权重(默认 `Xenova/bge-base-zh-v1.5` 约 **389 MB**,之后离线可用)。即使装不上也只是语义路径降级,不会报错。 - **可选自愈**:`semantic.self_heal.enabled: true` 时,插件用底座的 `runtime-deps` helper(`mem:semantic-deps`,base ≥ 0.7.0 / interface v6)把缺失的 `@huggingface/transformers` 装进**共享根** `/deps/runtime/<内容键>/`,再经 `createRequire(<根>/package.json)` 加载。安装按 `npmmirror → npmjs → 本地缓存(/cache/npm,离线)` 顺序进行,每个 registry 后各重试一次 `ONNXRUNTIME_NODE_INSTALL=skip`(绕开 `onnxruntime-node` postinstall 的 302)。失败不阻塞挂载、也不阻塞语义预热:运行时回退到自己的 optional 解析,仍不可用就降级 FTS+entity。默认**关闭**,因为打开即意味着启动期可能联网;共享根不可用/底座过旧时同样回退。配置键:`enabled` / `packages` / `sources` / `attempts` / `verify`(见 [INSTALL §5](docs/INSTALL.md#5-语义检索配置默认已启用-onnx-模型))。 - **启动依赖审计 + 会话提示**(`res/runtime-deps.yaml` 的 `audit` 段,随包发布):每个条目说明**观察什么**(`kind: module` + `specifier`:真加载该包,含原生绑定;`kind: runtime-deps` + `item`:观察该 item 的状态)、**多大声**(`severity`)、**什么条件下说**(`when`:`failed` / `missing` / `degraded`;`pending` 与 `unknown` 一律不说)以及**说什么**(`body`)。审计在挂载最后阶段跑一次、self-heal 项结算后再更新;`agent/pre-step` 只 consume,不在提示路径上加载任何包。提示注入**根会话**的带用户消息的那一步,和 §12 的相关性提示合并成**一条**消息,同一会话每条最多一次;子 agent / worker 会话不注入(底座的 `ownerOnly` 语义)。文案、级别、条件全部来自这份声明:代码里没有包名分支,也不带默认文案;声明文件缺失/不合法会打 ERROR 并降级(不声明 item、不注册提示),插件照常挂载。 ### 语义检索依赖修复(`degraded: true`) `@huggingface/transformers` 是 optional dependency;它的传递依赖 `onnxruntime-node` 带 `postinstall`。如果 DSH profile 没有链接 optional dependency,fuzzy 搜索会显示 `degraded: true`,日志只有 FTS/实体腿。**首选**在 profile 配置里打开自愈(上一节的 `semantic.self_heal.enabled: true`),重启 dsh 后看 `envinit: mem:semantic-deps present|installed`。 需要手工处理时按下面顺序修复: ```bash cd ~/.dsh/profiles/web # 1) 国内镜像优先 npm_config_registry=https://registry.npmmirror.com \ pnpm add @huggingface/transformers@^4.2.0 # 2) onnxruntime postinstall 报 302 / Failed to download build list 时跳过下载检查 ONNXRUNTIME_NODE_INSTALL=skip \ npm_config_registry=https://registry.npmmirror.com \ pnpm add @huggingface/transformers@^4.2.0 # 3) 可选原生加速;失败分别退化为 numpy / 正则 npm_config_registry=https://registry.npmmirror.com \ pnpm add hnswlib-node@^2.1.1 nodejieba@^2.6.0 ``` 镜像顺序:`registry.npmmirror.com` → `registry.npmjs.org` → pnpm store 缓存。 模型源默认 `https://hf-mirror.com`,可用 `HF_ENDPOINT` / `AVANTF_MEM_MODEL_MIRROR` 覆盖。 重启 dsh 后,fuzzy 结果应为 `degraded:false`;详细排错见 `docs/INSTALL.md` §7.3。 - 语义后端负责嵌入,`rerank` 提供可选的第二阶段重排;不可用或失败时恢复正常腿门槛及融合排序,结果显式标注 fallback。普通并发重排在总超时预算内排队;已超时但底层尚未结束时不再启动新推理。 - **换嵌入表示 = 一次数据迁移**(重要):库里持久化的向量带着**表示指纹**(空间 id = `v2///@p=;n=<0|1>;w=<配置截断窗口>;r=<模型旁车 sha>`;pooling / normalize / 截断窗口 / 模型权重任一变化都必须让 id 不等)。换默认模型、换维度、改 `semantic.max_input_tokens`、换池化/归一化,甚至同一模型仓库换了权重,旧向量都属于**旧坐标**,语义腿对它们不可用,检索会**静默退化成词法+实体**——看起来仍有结果,只是答错。实测:默认模型 `bge-small-zh-v1.5`(512 维)→ `bge-base-zh-v1.5`(768 维)后,本机 80 条 ACTIVE 里 **78 条**被跳过、`indexed` 只剩 **2**,修前多条查询塌成同一条无关短事实。行为: - **检测 + 响亮告警**:启动时若存在旧空间向量,日志打一条 WARNING(条数 + 原因 + 手动入口);`/mem` 状态面、`avantf-mem stats`、`avantf-mem vectors`(`vectors.{stale,space_stale}`)如实暴露计数。 - **自动自愈**:插件在启动预热之后**分批**重算(每批有界、逐批让出事件循环、不阻塞查询、可续跑、失败下轮重试)。手动入口 `avantf-mem vectors --fix`(先 `--dry-run` 预览计数;GUI 设置页的「向量索引」也调用同一条 runtime 修复;模型面不再暴露 `vectors_fix`)。 - **指纹格式换代 = 一次性全量重编码**:本次升级把空间 id 换成带版本前缀的表示指纹(`v2/…`),所以**每个**老库升级后第一次启动时全部向量都记为 `space_stale`(告警会写明 "written before the representation fingerprint — a ONE-TIME full re-encode"),随后由同一套有界后台迁移整库重算一遍(本机 80 条 ACTIVE 实测数秒级)。这是预期代价,不是故障;迁移必须 **幂等**——再跑一次必须 `migrated: 0`。 - **模型 revision 读不到时只降级**:没有底座旁车(`<模型缓存根>/.envinit/models----/record.json`)时指纹不含 revision;**权重已在本地**而旁车缺失时,启动日志会有一条 `semantic: no model revision …` 的 WARNING 说明"同名仓库换权重"这一盲区(首次安装进行中则不报,那时还没有可指纹化的东西);绝不影响打开库或其余旋钮的检测。 - **指纹必须在进程内稳定**:`w` 取**配置值**而不是解析后的窗口(解析窗口依赖已加载的分词器,预热前后会变 ⇒ 会让 store 把自己刚写的行判 stale);后端未声明表示时记 `rep=undeclared` 而**不是崩溃**。自愈必须用 **真实形状**验证:库里放**旧维向量**再上新模型,断言自愈后语义腿重新命中(只复刻"1 条正确答案 + 1 条干扰 项"不算)。 - **关闭自动迁移**:`semantic.auto_migrate: false`(默认 `true`);关掉后只剩告警 + 手动入口。 - agent 工具统一返回 `{ok:true,result}` / `{ok:false,error,violations}`(DSH 工具与 MCP server 一致);`db.path` 里的 `~/` 展开为**用户 home**,留空则落在数据目录下。 - **模型面是 3 个工具**:`mem_remember` / `mem_recall` / `mem_admin`。`mem_admin` 的模型面只有 `list` / `contradictions` / `export`(`import` / `restore_snapshot` 是不可逆的批量写,**只在 CLI / GUI 上**由操作者执行);诊断、策展(pin/archive/矛盾裁决)与修复动作同样只在 CLI / GUI / runtime, 完整联合在契约的 `AdminUnion` / `RecallUnion` / `RememberUnion` 里保留。`mem_recall` 的模型面字段同样收窄 (`track` / `max_tokens` / `floors` / `include_scores` 只属于内部全参入口,见 `DESIGN.md` §10), `search` 新增可选 `alternates`(1–4 条换说法,只作用于 `fuzzy` 段、候选并池后只重排一次)。 `mem_remember` 只有 add/update/remove,`mem_recall` 只有 search/titles 两个入口:`search` 的关键词查询返回**分段结果、没有 `mode` 字段**——`exact`(内容连续包含 query 的字面条目,分页 20/页、`offset`/`limit` 只对它生效)、 `fuzzy`(三路召回 → 重排 → 固定 5 条,总是跑)以及可选的 `relations`(关系问句走到路径时); 纯多实体问句仍是独占的 `mode:"facts"`;只给 `time`、不给 `query` 时是**纯时间召回**(`mode:"time"`:按时间轴从新到旧、 **默认一页 50 条**(上限 50),窗口更长时结果带 `remaining`=还有多少条未显示,且不套默认输出预算, 不做字面/关系/模糊检索);`titles` 先按标题完整相等匹配(大小写敏感),没有才做模糊标题检索 (默认 active + journal,返回候选而不返回相关性分)。时间条件用 `search.time` / `titles.time`, `mem_admin list` 也接受同一个 `time` 过滤。 三个文件动作(`export` / `import` / `restore_snapshot`,后两个只在 CLI / 面板上可用)的 `file` 都**只允许本次运行的数据根内**(默认导出到 `<数据根>/backups/memory-YYYYMMDD-HHMMSS.json.br`,原子写;导出结果不含文档正文)。 `import` 按内容并集、可重复,并按**文件头**区分:SQLite → 只读合并另一份 `memory.db`(报告 `mode:'db'`), 其余 → 导出文档(`mode:'json'`);`restore_snapshot` 只写入**空 facts 表**,必须显式 `confirm: true`, 恢复后同一宿主进程里的检索立即命中新库(无需重启)。数据根之外的路径用 CLI。 ## 自定义系统提示词 注入给模型的 systemPrompt 段落的**正文**可以自己改,一段一个文件。它们放在数据根的共享提示词目录里, 用**文件名前缀**区分归属;本插件只读写自己的 `mem-*` 文件,目录里其它 `.md` 不读、不写、不删: ``` ~/.avantf/prompts/ # 共享提示词目录 └─ mem-memory-usage.md # 记忆:何时该记、不要记什么(order 3000) ``` 每个插件**只读写自己前缀的文件**:别人的文件、以及任何不在清单里的 `.md`,既不会被读、也不会被写或删。 - 插件启动时**自动创建**属于它的那几个文件,内容就是内置的默认提示词;**空文件会被重新填回默认**(不是"禁用这一段"),删掉也会重建。 - **文件全文就是提示词**:不要写标题、注释或 frontmatter —— 它们会一字不差地进入每一轮 prompt。要给别人看的说明写在这里,不要写进文件。 - **只在插件初始化时读一次**:改完要**重启 dsh** 才生效(和 host 半边改代码同一条规则)。段落名与注入位置(order)由代码固定,改文件名不会换位置;目录里其它 `.md` 不会被读取,也不会报错。 - 自己写的文本只做一次**软检查**(超过 400 字、出现"衰减/遗忘/强度"这类保留机制词、出现"因为/否则"这类解释措辞)并各记一条警告,**不截断、不拒绝** —— 这三类是实测过的退化形状,提醒一句而已。 - 文件读不了或写不了(只读文件系统、权限不足、路径被目录占住)只记一条警告并退回内置默认,**绝不影响插件挂载**。 ## 强度与遗忘(自然消退 / 召回加强 / 永久记忆) 记忆按**活跃使用日**老化(不是挂钟):进程启动与心跳推进“活跃日”,每次最多计 `strength.presence.gap_cap_days`(默认 1 天),所以**停机 90 天只相当于老了 1 天**,而常开时与日历 1:1。 - 新事实 `strength = 0.5`,起始强度与锚点都冻结在 0.5 ⇒ **`base_lifetime_days`(默认 90)个活跃日到 0 即归档遗忘**(`forgot`); - **召回会加强**:低于 `0.5` 每次 `+0.1` 直到抬回 `0.5`(5 次),高于 `0.5` 每次 `+0.01`,封顶由 `max_retention_days`(默认 174,accelerating 形状下 = ceiling `0.7492` ≈ 0.75,即 174 个活跃日)反解; 每条事实每 24h 最多 3 次有效加强(按这个配额:0.05 起 2 天回基准、0.5 起 9 天登顶),且**召回永远不会变成永久**; - **最长寿命是 174 个活跃日**(默认值,P4 起):比 P3 的等价档(153)多 21 天,这是一次**有意的默认承诺变化**, 回滚成旧行为只需把 `decay_shape` 改回 `linear`、`max_retention_days` 改回 `153`(见 [CHANGELOG/unreleased.md](CHANGELOG/unreleased.md)); - **永久记忆**:只有一条路——**显式 `pin`**(`mem_admin pin` / `avantf-mem pin ` / 设置页「永久记忆」), strength 定格 1.0;固化后**不结算、不被任何自动路径归档**(`SETTLE_PREDICATE` 与唯一的自动归档 `archiveForgotten` 的谓词都带 `pinned = 0`,purge 侧见 `purgeBranches`),退出需显式 `unpin`; - **想压掉一条不想要的事实**:用 `archive`(CLI `avantf-mem archive ` / 设置页「归档」);「这条哪天起不再为真」 也只能这样表达——没有单条到期字段、也没有到期日标注,`update` 覆盖旧版本或 `archive` 移出,仅此两条; - **没有日历兜底**:寿命只由活跃日决定;低频机器上活跃日远少于日历日,陈旧事实会留得久一些 (换算见 [docs/STRENGTH_MODEL.md](docs/STRENGTH_MODEL.md) §10),清理靠显式 `archive` / `mem_admin maintenance`; - `strength.enabled: false` = 停用衰减 / 加强 / pin **与自动归档**,只剩 purge 回收已归档行;CLI 会提示「自动归档已停用」; - 排序不受影响:strength **不参与**检索排序(35 查询评测基线是精确断言)。 ```bash avantf-mem strength # 诊断:活跃日时钟 / 永久数 / 今日加强量 / 待遗忘数 avantf-mem pin 12 # 固化为永久记忆 avantf-mem unpin 12 # 解除永久 avantf-mem maintenance # 强制跑一次完整生命周期 pass(结算/遗忘/清理) ``` ```yaml # ~/.avantf/configs/common.yaml strength: base_lifetime_days: 90 # 锚点:0.5 起点 → 0 的活跃日数(新事实不被召回时的寿命) max_retention_days: 174 # 召回加强最多能换来的活跃日数(accelerating ⇒ ceiling ≈ 0.7492) decay_shape: accelerating # accelerating(默认)| linear(回滚档:配 153 时 ceiling 回到 0.85) decay_curvature: 0.05 # accelerating 的下限项 a,合法区间 (0, 0.5];linear 下不参与 recall_gain_below_base: 0.1 # 低于 0.5 时每次召回加多少(封顶 0.5:0.05 起 5 次回到基准) recall_gain_above_base: 0.01 # 高于 0.5 时每次召回加多少(封顶上面反解出的 ceiling) recall_daily_cap: 3 # 每条事实每 24h 最多加强次数 presence: gap_cap_days: 1 # 每次在场最多计 1 天(停机不饿死记忆) heartbeat_minutes: 60 # 长驻进程的心跳间隔;0 = 只在启动时接续 enabled: true # false = 停用衰减/加强/pin 与自动归档(purge 仍跑) ``` 完整规格与决策记录见 [docs/STRENGTH_MODEL.md](docs/STRENGTH_MODEL.md),概要与运维见 [DESIGN.md](DESIGN.md) §18。 ## 树内约定(开发) - **契约唯一真源**:`@avantf/mem-contract` 是工具 / 配置 schema 与 UI payload 的**唯一真源**——工具 schema、 MCP `inputSchema`、CLI 参数、client 类型都由它派生。 - **检索编排不许分叉**:工具键 → 运行时的派发表只在 `packages/core/src/dispatch.ts`,检索流程只在 `packages/core/src/store/hybrid.ts`,**不许再分叉**。 - **融合必须让证据按可靠性定序**:确定性 / 高精度信号(意图表改写、精确词法命中、结构化字段)排在前, 稠密相似度只能作补充;**绝不允许"某条腿的偶然头部"压过"另一条腿的压倒性证据"**——各腿按自身最大值归一 后**不可直接比较**(`packages/core/src/store/floors.ts` 已注明融合分只能同查询内比较),"某腿拿了 1.0"不 构成"最相关"的证明。改融合或门槛时,必须同时给出"**并集不收窄**、**非自指查询逐字节不变**"的证据,并 **先回答"这条候选是靠哪条腿、几分证据进来的"**。 - **读 zod 内部别猜**:用 `def.shape` / `def.values` / `z.toJSONSchema(..., { io: 'input' })` 这类公开形状, 不读私有实现;形状断言在 `packages/contract/test/contract.spec.ts` 与 `tool_schema.spec.ts`。 - **模型工具面就是 3 个**:`TOOL_SPECS` 里的 `mem_remember` / `mem_recall` / `mem_admin`;挂载冒烟断言的 就是这 3 个,`kb_*` 工具已随知识库迁到 `dsh-knowledges`。 - **类型检查顺序**:`pnpm typecheck` 也检查 `test/`,且必须在 `pnpm build` **之后**跑(包通过 产出的 `lib/*.d.ts` 读依赖);`pnpm typecheck:dsh` 覆盖插件 src + specs。 - **中文评测集是精确断言**:`packages/core/test/eval_zh.spec.ts` 共 **41 条**(原 35 + 自指问句哨兵 6〔4 条表内 + 2 条表外 KNOWN GAP〕)的汇总数字是**精确 断言**——任何移动它的改动都要重新冻结并解释。 - **CHANGELOG 目录规则**:`CHANGELOG/unreleased.md` 是唯一在写的文件;每个已发布版本是 `CHANGELOG/.md`。发版时把 `unreleased.md` 改名为 `CHANGELOG/.md`,再新建一个空 `unreleased.md`。严格门禁要求当前版本文件存在、`unreleased.md` 没有 `### ` 条目。 - **枢纽文件(hub)**:超过 800 行的枢纽是 `packages/core/src/store/memory.ts`(SQL/索引形状含实测性能结论, **慎动**)、`packages/plugin/src/client/index.ts`、`packages/core/src/db/dao/facts.ts`、 `packages/contract/src/types.ts`、`packages/plugin/src/index.ts`;通用的 "触及即抽 / 不为变小做整体重构 / 名单只作观察"规矩在本仓同样适用。 ## 发布 `packages/plugin` 声明了 `publishConfig.access=public`;它的 `prepublishOnly` 会在真 `publish` 时先跑本仓 共享 `build/scripts/prepublish-assert.mjs` 的发布面断言、再跑 `pack-plugin.mjs`。 **打 tag 之前先跑一次发布门禁**(它会检查各组版本一致、`CHANGELOG/.md` 已存在、`unreleased.md` 已清空, 并依次跑 frozen-lockfile / build / typecheck / test / `typecheck:dsh` / `build:dsh`+mount smoke): ```bash pnpm release:check ``` 底座 `@avantf/dsh-plugin-base` 是插件的**普通运行期依赖**(`dependencies`,区间 `>=0.3.0 <1.0.0`);插件同时把它声明进 `devDependencies`(同一条),所以 `pnpm install` 会从 registry 解析一份,而本地构建用 `pnpm link:base`(`../base` 或 `$AVANTF_BASE_DIR`)把插槽换成 checkout。`pnpm build:dsh` 会先 `build-base`、再跑 `link-envinit`,从**已链接的 checkout** vendor 它 零依赖的 bootstrap;底座**绝不内联、也绝不按 specifier import**。**装插件就自动带上底座**,用户不需要单独装它(dsh 给每个 profile 写 `autoInstallPeers: false`,所以这里也不能用 peer 表达"宿主提供"): ```jsonc // ~/.dsh/profiles//package.json —— 只点名插件即可 "dependencies": { "@avantf/dsh-mem": "^0.1.1" } ``` 缺了底座只是降级挂载(一条 `envinit: WARNING` + 退回 legacy 机制,绝不拒载)。 宿主自己提供的 peer 才是 `optional`,免得包管理器去 registry 拉一份宿主内部实现。环境初始化框架与启动兼容 门禁现在都在底座**这一个包**里(不再有独立的 `@avantf/dsh-envinit` / `@avantf/dsh-compat`,也不再有 `mem:compat` item 或受管 `runtime` 根上的门禁副本)。**接口世代由底座在运行期裁决**:插件构建期把 `INTERFACE_VERSION` bake 进 `lib/interface-version.json`,启动时用底座自己的 `readInterfaceRequirement` 读回、再调 `checkInterface`;`incompatible`(区间内但另一世代)⇒ 一条 `WARNING` 且**不用底座的共享能力**(自带 prompt 默认正文、门禁跳过、legacy provisioning)但**照常挂载**, `cannot-tell` ⇒ 只告警、照常使用 —— 接口变不再要求插件同批改依赖区间。发布顺序是**底座先于插件**—— `release-check` 在发布插件前会确认 registry 上已有落在插件依赖区间内的底座版本;发布前确认 发布 manifest 里没有 `link:`/`file:`——本地链接只由 `link-base` 脚本完成,`pack-plugin.mjs` 会拒绝仍带 这类 specifier 的 tarball。 后三步(链接 / `typecheck:dsh` / `build:dsh` + mount smoke)**只对着已安装的全局 `dsh`** (`npm i -g @deepseek-ai/dsh`):邻居包从它软链,`tsc` 因此对着你真正运行的那一份检查,tsdown 的 client preset 用仓库自带的 pin 住的副本(`packages/plugin/vendor/dsh-client-preset/`,见 `ORIGIN.md`)。 所以**没有 harness 源码也能跑完整个门禁**: ```bash pnpm release:check # 7 步全跑,LOCAL 三步走已安装的 dsh pnpm build:dsh # 只重建插件时同理 DSHHARNESS=/nonexistent pnpm build:dsh # 反证:没有任何编译路径会去解 harness ``` **preset 副本的漂移会自己报出来**(可选核对,不参与编译):本地刚好有 harness checkout 时, `link-dsh` / `release:check` 会逐个字节比较那 8 个自带文件与 checkout 的对应文件,不一致就打出是哪些 文件、两侧 revision,并指向 `ORIGIN.md` 的重新对齐步骤(`link-dsh` 打 `WARNING`、release-check 打 `note:`,都不是硬失败——checkout 领先于 pin 是重新对齐前的正常状态);没有 checkout 时什么都不打。 也可单独跑 `node scripts/check-preset-drift.mjs`(有漂移时 exit 1)。 发布**直接从本仓做**:不再投影到别的 release 树,也不再有"发布树"这一步 (第五轮复审 §1.5/§7.11;唯一权威流程是 `docs/RELEASING.md`)。发布前跑发布前断言,再走严格门禁: ```bash pnpm prepublish:assert # 发布面不变量(只读) pnpm release:check # 严格门禁(含 link:base/build:base/pack/old-dsh) ``` `prepublishOnly` 也把这条断言串进真 `publish`(见 `packages/plugin/package.json`),所以不会漏跑。 共享 `build/scripts/prepublish-assert.mjs` 只读、不构建、不打包、不联网,断言:`packages/plugin` 是唯一的可发布包、 版本只记在**一份** manifest 里且 `CHANGELOG/.md` 已切、包随自己的 README(首行是包名)、依赖段没有 `link:`/`file:`/`workspace:`、`files` 不装进发布工具链。 本包的 npm 页面就是 `packages/plugin/README.md` 本身(真实文件,`files` 里有它)。插件的 manifest 在开发树里就是要发布的那一份(引擎与 `react` 放 `devDependencies` 以便内联,`dependencies` / `optionalDependencies` 就是引擎的运行时依赖面;底座 `@avantf/dsh-plugin-base` 是**普通运行期依赖**, 装插件即自动带上,本仓另在 `devDependencies` 里声明同一条区间以便 `pnpm install` 链到本地 `base/`)。 > 这份 manifest 形态是**构建正确性的一部分**:tsdown 的规则是"production 段保持 > import,其余全部内联",所以引擎一旦回到 `dependencies`,`lib/index.js` 就会悄悄**不内联**——这种产物 > 在本仓库里照样构建、照样通过 mount smoke(本仓 workspace 的 node_modules 能解析 `@avantf/*`), > 但**拷进任何 DSH profile 就起不来**(`Cannot find package '@avantf/mem'`)。`pnpm run pack` > 就是为这条断言存在的,而且它在**开发树里就成立**:`pnpm build:dsh && pnpm run pack` 可以在本地 > 就把"用户装的那一个包"验完。`pnpm release:check` 会带上它,并真实安装+挂载打出来的 tarball。 **版本、升级与回滚、release notes 必须带上的已知限制、以及人工确认清单**都在 [docs/RELEASING.md](docs/RELEASING.md)。要点:本仓所有包随同一次发布(版本只记在 `packages/plugin/package.json`;其余 7 个私有 manifest 不带 `version`),但**只发布** `@avantf/dsh-mem`(引擎内联进 `lib/index.js`;运行期只有底座这一条普通依赖, `@avantf/dsh-plugin-base` 装插件时自动带上,**底座必须先于插件**发布);迁移**单向**(升级前备份 `~/.avantf`,降级会被 `SchemaDowngradeError` 拒绝);插件的 DSH peer 范围就是"支持的宿主窗口"。 另外 7 个包(引擎 / `mem-cli` / `mem-mcp`)在 manifest 里都是 `private: true`:它们只作为本仓库的 源码依赖存在,`pnpm -r publish` 碰不到它们,`release:check` 的 preflight 也会断言"可发布的只有 plugin"。 发布本身(npm registry)是独立动作。**先打包、再发 tarball**——直接 `pnpm -C packages/plugin publish` (或 `pnpm --filter @avantf/dsh-mem publish`)在 pnpm 12 下必失败:`publish` 内部那次 pack 要把 `workspace:*` 落成版本号,而引擎包按约定 `private` 且无 `version`,于是报 `ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`(`pnpm run pack` 用 `withWorkspaceVersions()` 在打包期间临时补版本, `publish` 那一次没有这层包装;`docs/RELEASING.md` 有最小复现): ```bash pnpm run pack # → release/avantf-dsh-mem-.tgz npm publish "$PWD/release/avantf-dsh-mem-.tgz" --access public \ --registry https://registry.npmjs.org/ # 只这一个包 ``` `npm publish ` 不跑生命周期脚本(断言已由 `pnpm run pack` 跑完),也不做 workspace 解析, 所以它不受上面那条 pnpm 限制。细节见 `docs/RELEASING.md` §2 的四条前提。 npm 现在默认走 **staged publish**:`publish` 打印 `✅ Published package …` 且 exit 0,但版本只是**暂存**、 要你在 npmjs.com 上批准才上线(批准前 `npm view <包>@<版本>` 是 404、`latest` 仍指旧版,再发同版本报 409)。 发完用 `dist-tags.latest` 确认,别信那句 "Published"。 ## 快速开始(开发) ```bash pnpm install --frozen-lockfile pnpm build:dsh # 测试(引擎包:contract / retrieval-core / core / cli / mcp) pnpm test # 记忆 CLI export AVANTF_HOME=$(mktemp -d) node packages/cli/lib/index.js add "张伟管理李娜" node packages/cli/lib/index.js search "李娜" node packages/cli/lib/index.js list ``` ### 编译 / 校验 / 打包命令一览 产物只有两类(本仓库不使用 `incremental`/`composite`,没有 `*.tsbuildinfo` 之类中间缓存): - **引擎包的 `lib/`**:`pnpm build` 产出,5 个包各自 `packages//lib/`(`*.js` + `*.d.ts` + 两种 `.map`); - **插件的 `lib/`**:`pnpm build:dsh` 产出,`lib/index.js`(host 半边)、`lib/client.js`(browser 半边)、 `lib/types/**`(`tsc` 产物,同时是 `tsdown` 的输入——运行时只加载前两个);打包产物 `release/*.tgz` 由 `pnpm run pack` 生成。 #### 一、构建 | 命令 | 作用 | 产物 / 影响 | |---|---|---| | `pnpm run setup` | 统一构建契约:`pnpm install --frozen-lockfile` | `node_modules/`;外部 meta 脚本按这个名字调用 | | `pnpm run compile` | 统一构建契约:等价 `pnpm build:dsh`(一条命令完成编译) | 同 `build:dsh` | | `pnpm install --frozen-lockfile` | 装本仓依赖(根 + `packages/*`) | `node_modules/`;`autoInstallPeers: false`,所以 `@deepseek-ai/*` 不入依赖图,由 `link-dsh` 软链。底座从 registry 解析一份占位,随后由 `pnpm link:base` 换成 checkout | | `pnpm link:base` | 把 `../base`(或 `$AVANTF_BASE_DIR`)链到 `packages/plugin/node_modules/@avantf/dsh-plugin-base` | 只换这一个插槽;`--check` 只报告不改 | | `pnpm build:base` | 构建底座 checkout(缺 `node_modules` 时先 `--frozen-lockfile --ignore-scripts` 安装) | `/lib/`;`build:dsh` 的第一步 | | `pnpm build` | **5 个引擎包**(contract / retrieval-core / core / cli / mcp)的 tsc 编译 | `packages//lib/`。**不含插件**(filter 排除了 `@avantf/dsh-mem`),因为插件构建对着已安装的 dsh | | `pnpm build:dsh` | **一条命令完成编译**:先 `build:base`(必要时装 base 依赖并构建);`build-plugin` 自动判断首次/重建,必要时装本仓依赖 → 构建引擎包 → `link-dsh` → `link-base` → vendor bootstrap → 插件 `tsc + tsdown` → 引擎/插件 typecheck → `test:dsh` → mount smoke | `packages/plugin/lib/{index,client}.js` + `lib/types/**` | | `pnpm build:dsh --fresh` | 强制走首次编译(产物缺失/损坏时) | 同上 | | `pnpm build:dsh --skip-deps` | 完全不碰引擎包(只改插件时更快) | | | `pnpm build:dsh --no-verify` | 跳过构建后的 mount smoke | | | `pnpm rebuild:dsh` | `build:dsh` 的别名 | | | `pnpm -C packages/plugin run bundle` | UI 半边的逃生口:纯 `tsdown`(**不做类型检查**、不跑 mount smoke),**改设置页 UI 用这条,无需重启 dsh**;preset 恒用仓库自带的副本,所以在没有 harness 源码的机器上也能跑 | `lib/{index,client}.js` | | `pnpm -C packages/plugin run build` | 插件的原始构建(`tsc + tsdown`),就是 `build:dsh` 的第 ③ 步;`tsc` 对着 `link-dsh` 链好的(已安装 dsh 的)类型检查 | `lib/{index,client}.js` + `lib/types/**` | | `pnpm -C packages/plugin run watch` | `tsdown --watch`:持续重建两个半边;UI 改动自动热替换,host 改动仍需重启 dsh | | | `pnpm -C packages/ run build` | 单包编译(`` ∈ contract / retrieval-core / core / cli / mcp) | 该包 `lib/` | #### 二、类型检查与测试 | 命令 | 作用 | 备注 | |---|---|---| | `pnpm typecheck` | 5 个引擎包的 **src + test** 类型检查(`tsc -p tsconfig.test.json`) | 无产物;测试类型也是门禁 | | `pnpm typecheck:dsh` | 插件的 **src + test** 类型检查 | **需要已安装 dsh 的链接**(`link-dsh`),所以 CI 不跑、只在本地跑 | | `pnpm test` | 6 个包的 vitest(含插件) | | | `pnpm -C packages/ run test` | 单包测试 | | | `pnpm -C packages/ run typecheck` | 单包类型检查(含 test tsconfig) | | #### 三、打包与发布门禁 | 命令 | 作用 | 产物 / 备注 | |---|---|---| | `pnpm run pack` | 打出"用户安装的那**一个**包"并断言自包含:引擎已内联、依赖里没有 `@avantf/*`、`catalog:` 已落成真实范围、`files` 含 `lib`+`README.md` | `release/avantf-dsh-mem-.tgz` | | `pnpm run pack --mount` | 再把 tarball 解进临时 profile **真实安装并挂载**(需要已安装的全局 dsh) | `--out ` 换输出目录(默认 release/),`--keep` 失败时保留 scratch | | `pnpm release:check` | 发布门禁一条命令:preflight(版本只记一处 / `CHANGELOG/.md` 已切 / 只 plugin 可发布 / 声明了 DSH peer)→ frozen-lockfile → link:base → build:base → build → typecheck → test → 插件 typecheck → `build:dsh`+mount smoke → `pnpm run pack` → registry 底座探测 → `check:old-dsh` | 打 tag 前必跑;`--allow-uncut` 用于切版本文件之前的预跑,`--offline` / `--allow-missing-base` 用于离线与发布前试跑 | | `pnpm prepublish:assert` | 发布前断言:唯一可发布包 / 版本只记一处 + `CHANGELOG/.md` 已切 / 随自己的 README / 没有 `link:`/`file:`/`workspace:` / 没有包装进发布工具链 | 只读;真 `publish` 时 `prepublishOnly` 也会跑它;`--tarball` 追加字节断言 | | `pnpm version:set X.Y.Z` | 切版本:**只改一个文件** `packages/plugin/package.json`(其余 manifest 不带版本) | `pnpm version:check` 会拦"私有包又长出版本号",`pnpm release:check` 也会 | | `pnpm prepublish:assert --tarball release/avantf-dsh-mem-.tgz` | 追加"真实产物字节"断言(不带发布工具链、README 首行是包名、`catalog:` 已落成真实范围) | 在 `pnpm run pack` 之后 | | `npm publish "$PWD/release/avantf-dsh-mem-.tgz" --access public --registry https://registry.npmjs.org/` | 真正发布(**只这一个包**,发的是刚打出的 tarball) | 其余引擎包是 `private: true`,`pnpm -r publish` 碰不到;**不要**改回 `pnpm --filter @avantf/dsh-mem publish`(pnpm 12 必失败,见上文发布一节) | #### 四、基准与辅助 | 命令 | 作用 | |---|---| | `pnpm bench:vstore` | 向量库规模基准——`vectorStore.auto_thresholds.hnswlib`(默认 2000)的选型依据,同时量 ANN 的召回损失 | | `pnpm bench:memory` | 记忆库规模基准——检索腿随语料增长、`remember update` 在 hnswlib 之后的 O(N)、启动重建 ANN | | `pnpm bench:indexes` | 索引 A/B 实验台,含一个被实验证伪的部分索引反例 | | `pnpm bench:reinforce` | 读路径写放大——`search`(`track: true`)触发的语句数与 WAL 字节、开/关 `track` 的延迟对比 | | `node scripts/mount-smoke.mjs` | 独立挂载自检:真实 Cordis 上下文挂载插件 + 降级挂载(坏 `dataHome`);peer 恒取自已安装的全局 dsh,期望输出 `MOUNT SMOKE OK` | | `node scripts/link-dsh.mjs [--runtime [dir]] [--no-bake]` | 重建插件的 `@deepseek-ai/*` 软链接,**来源恒为已安装的全局 dsh**(`npm root -g`;`--runtime ` 显式换成另一份安装,`--no-bake` 只链接、不写"编译对着谁"的记录),让插件与宿主共用同一批 cordis/schemastery 实例。同时给自带 preset 根发布 manifest stub(`packages/plugin/vendor/dsh-client-preset/packages/client/avantf-dsh-mem/package.json`,指向本插件 `package.json` 的软链) | | `node scripts/check-preset-drift.mjs` | 逐个字节比较自带的 8 个 client-preset 文件与 harness checkout;有漂移时列出文件与两侧 revision 并 exit 1,没有 checkout 时什么都不比、exit 0 | | `pnpm clean` | 删 `packages/*/{lib,dist}`、根 `release/*.tgz` 与 `node_modules/{.cache,.vite}`;之后 `pnpm build:dsh` 会走首次编译路径 | | `pnpm cleanup:dsh [--dry-run\|--yes\|--keep-deps\|--profile

]` | 从 dsh profile 卸载插件并清 `cordis.patch.yml`(不动 `~/.avantf`,也不动仓库) | > 各命令的完整旗标以 `--help` 为准:`pnpm build:dsh --help`、`node ../build/scripts/release-check-engine.mjs --help`、 > `bash scripts/cleanup.sh --help`;发布前断言、版本、跨版本门的入口都在本仓 `scripts/` 与 > `docs/RELEASING.md`。 > 编译产物的清理、插件两个半边的生效规则、链接来源与 client preset 的细节见下文各小节。 ### 知识库的入库边界与 domain 清单(已迁出) `kb_ingest`/`kb_query` 的摄入边界(`knowledge.ingest.local_roots` / `allow_outside_workspace` / `allow_private_network`)与领域清单(`knowledge.domains`)现在都由 **`dsh-knowledges`** 读取与校验。 `~/.avantf/configs/knowledge.yaml` 文件原样保留,由新插件接管;本插件不再读它。 ### DSH 插件构建 `@avantf/dsh-mem` 是一个**标准 DSH Cordis 插件**(不是动态包)。它引用 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-typert-protocol`,而后者的传递依赖并未全部单独发布, 因此需要在 **DSH harness 工作区内部**构建: ```bash pnpm build:dsh # 自动判断:没编译过就首次编译,编译过就重建 node scripts/mount-smoke.mjs # 挂载验证(harness 自动发现;必要时用 DSHHARNESS 覆盖) ``` 引擎包(`@avantf/mem-*`、CLI、MCP)可独立构建与测试;只有 DSH 插件包 (`@avantf/dsh-mem`)与 harness 工作区耦合。 ### DSH 插件:改完源码后怎么编译、怎么生效 插件的 `~/.dsh/profiles/web/package.json` 用 `link:` 直接指向本仓库的 `packages/*`, **没有拷贝步骤**:重建 `lib/` 即可。但插件有**两个半边**,生效方式不同: | 改了什么 | 需要重建 | 需要重启 dsh | 需要刷新页面 | |---|---|---|---| | `packages/plugin/src/client/**`(设置页 UI,→ `lib/client.js`) | `pnpm -C packages/plugin run bundle`(或完整 `build:dsh`) | **不用** | 通常不用 | | `packages/plugin/src/**` 其余(host 半边,→ `lib/index.js`) | `build:dsh` | **要** | 重启即整页重载 | | `packages/{core,contract,retrieval-core}/src`(引擎包) | `build:dsh`(自动重建变更的引擎包) | **要** | — | | `~/.dsh/profiles/web/cordis.patch.yml` | 不用 | **不用**(profile 配了 `patchReload: live`) | — | 生效规则来自 DSH 自己的两条链路(`dsh-web-app/cordis.patch.yml` 与 `dsh-base/cordis.patch.yml`): - **browser 半边**:`client-hmr` 行被**无条件挂载**,它每 **500 ms** `stat` 一次每个 `dsh.client` 行的 bundle 文件,mtime/size 变了就重算 rev,并通过 `/plugins/events`(SSE) 推送 `rebuilt` 帧,浏览器半边据此热替换该模块 → 重建即生效。 - **host 半边**:唯一的模块热重载通道 `hmr`(`@deepseek-ai/cordis-plugin-hmr`)在 `dsh-base` 里是 **`disabled: true`**,本 profile 没有打开;node ESM 模块在 profile 启动时 import 一次并缓存 → 必须重启 dsh 进程。 一条命令 `pnpm build:dsh`(= `node ../build/scripts/build-base.mjs && node ../build/scripts/build-plugin-engine.mjs`)先构建/链接 底座 checkout,再由 `build-plugin.mjs` 自动判断走哪条路: | 状态 | 模式 | 做什么 | |---|---|---| | `packages/plugin/lib/{index,client}.js` 缺失 | **首次编译** | 必要时 `pnpm install` → `pnpm build`(引擎包)→ 插件 tsc+tsdown | | 两个产物都在 | **重建** | 先只重建 `src/` 比 `lib/` 新的引擎包(都没变就跳过)→ 插件 tsc+tsdown | 两条路都会:从**已安装的 dsh** 链接邻居包 → 构建 → 挂载验证,最后告诉你还需不需要重启 dsh。约 5–7 秒。 ```bash pnpm build:dsh # 自动(首次编译 / 重建) pnpm build:dsh --fresh # 强制走首次编译 pnpm build:dsh --skip-deps # 完全不碰引擎包(只改插件时更快) pnpm build:dsh --no-verify # 跳过挂载验证 pnpm rebuild:dsh # build:dsh 的别名 ``` 只改 UI 时用 `pnpm -C packages/plugin run bundle`:纯 tsdown、**不做类型检查**、不跑挂载验证, `client-hmr` 会自动热替换;要类型检查就用完整的 `pnpm build:dsh`。 #### 清理编译产物 ```bash pnpm clean # 删掉 packages/*/lib、packages/*/dist、以及 node_modules 下的 .cache / .vite ``` `packages/*/lib` 是**唯一**的编译产物(6 个包,约 2 MB;插件只用 `lib/`,没有 `dist/`), 且本项目不使用 `incremental`/`composite`,所以没有 `*.tsbuildinfo` 之类会残留的增量缓存。 `clean` 后再跑 `pnpm build:dsh`,会自动走**首次编译**路径(模式判定见上)。 有两样东西 `clean` **故意不动**(它们不是编译产物,删了反而要重新搭环境): - `packages/plugin/node_modules/@deepseek-ai/*` 符号链接与 harness 里的 stub 目录 `/packages/client/avantf-dsh-mem/`:由 `link-dsh` 维护,重建插件时会重新发布; - `node_modules/.pnpm-*` 等 pnpm 自身的状态文件。 #### 接入 / 切换来源 / 移除(源码接入) profile 里的插件就是**一条 `link:` 依赖**(发布构建把引擎内联进 `lib/index.js`,开发构建从插件自己的 `packages/plugin/node_modules/@avantf/*` workspace 链接解析引擎,所以只需要 `@avantf/dsh-mem` 一条): ```bash # 接入一个 checkout(示例:本仓库 / 另一份 checkout) dsh plugin --profile web add link:/home/qunqi/opensource/avantf-mem/packages/plugin dsh plugin --profile web add link:/home/qunqi/opensource/avantf-mem-checkout/packages/plugin # 移除接入(只是把 profile 依赖删掉,不动挂载配置) dsh plugin --profile web remove @avantf/dsh-mem # 切换来源 = 先移除、再接入(同一条命令换路径) dsh plugin --profile web remove @avantf/dsh-mem dsh plugin --profile web add link:/home/qunqi/opensource/avantf-mem-checkout/packages/plugin # 看现在接的是谁 dsh plugin --profile web ls --depth 0 readlink -f ~/.dsh/profiles/web/node_modules/@avantf/dsh-mem ``` - **`cordis.patch.yml` 不用改**:它按包名 `@avantf/dsh-mem` 挂载,换来源只换 link 目标; - **必须重启 dsh 才生效**:宿主半边在启动时 import 一次并缓存,客户端模块图也是启动时构建 (`patchReload: live` 只让 patch 配置热加载,不会换模块)。重启之前运行中的进程不受影响; - `dsh plugin … add/remove` 只改 profile 的 `package.json` / `lockfile` / `node_modules`; **不在 manifest 里的残留 `@avantf/*` 符号链接 pnpm 不会回收**(切换来源后我遇到过三条),确认后手动 `rm`; - 换到另一份 checkout 后,改本仓库的代码**不再**影响运行中的 dsh: 要在那份 checkout 里 `pnpm build:dsh` → 重启 dsh。 #### 卸载插件(cleanup.sh) ```bash pnpm cleanup:dsh --dry-run # 先看计划与 patch diff,不改任何东西 pnpm cleanup:dsh # 交互确认后执行 pnpm cleanup:dsh --yes # 非交互 pnpm cleanup:dsh --keep-deps # 只改 patch,保留 profile 依赖 ``` 做两件事: 1. 从 `~/.dsh/profiles//cordis.patch.yml` 与**home 层** `~/.dsh/cordis.patch.yml` (dsh 会把它叠在每个 profile 之上)中,**外科式**删掉 `- id: avantf-mem` 这一项, 以及被删空的 `- insert:` 父项;其他插件条目、注释、缩进原样保留。 改前备份为 `cordis.patch.yml.bak.<时间戳>`,并且**双重护栏**:剩余 `- id:` 集合必须恰好 等于「改前 − avantf-mem」,改后文件必须仍能载入 dsh(dsh 要求顶层是 YAML **数组**, 所以删空时会自动补回 `[]`),否则整体中止且不写任何文件。 2. 通过 `dsh plugin --profile remove @avantf/dsh-mem @avantf/mem @avantf/mem-contract @avantf/mem-retrieval` 卸掉 profile 依赖(更新 package.json/lockfile 并删除 `node_modules/@avantf/*`);命令行里**只出现这四个包名**,不会碰其他插件。 **故意不动**(脚本结束时会逐条提示):`~/.avantf/**`(数据库 / 配置 / 受管资源)、仓库(源码、`lib/`、 `packages/plugin/node_modules/@deepseek-ai` 链接)、harness 里的 stub、`~/.cache/huggingface`。 要连数据一起清(不可逆,先备份): ```bash tar czf ~/avantf-data-$(date +%F).tgz -C ~ .avantf/memory .avantf/knowledge .avantf/configs .avantf/prompts .avantf/env rm -rf ~/.avantf/memory ~/.avantf/knowledge ~/.avantf/configs .avantf/prompts ~/.avantf/models ~/.avantf/env ``` > profile 配了 `patchReload: live`,所以 patch 一改,运行中的 dsh 会**立刻卸载**该插件(当前会话的 > avantf-mem 工具也会同时消失);建议之后冷启动一次 dsh 确认启动无报错。 > > 数据目录不是本插件独占的:`~/.avantf` 里还有旧项目的 `venv/ logs/ agents/ skills/ sessions/ > system.json` 等,**不要 `rm -rf ~/.avantf`**,只删 `memory/ knowledge/ configs/ prompts/ models/ env/` > (`env/` 是框架族根:受管的 `runtime/ tools/ models/`;`models/` 是 legacy 缓存目录)。 #### 链接:只有「已安装的 dsh」一个来源(重要) 插件要 import `@deepseek-ai/*`,而**编译与运行必须是同一份**: - 邻居包只有一个来源:**已安装的全局 dsh**(`node scripts/link-dsh.mjs`,从 `npm root -g` 下的 `@deepseek-ai/dsh/node_modules/@deepseek-ai/*` 软链)。插件与运行中的 profile 共用同一批 cordis/schemastery 实例,`ctx.typert.register` 与工具注册表都靠对象身份,链一份而跑另一份会在运行时 对不上。`pnpm build:dsh` 的第一步就是它,链接保持不动(不再有"构建时切到 checkout、之后再切回")。 - 编译**不需要 harness 源码**,也就没有第二种链接来源:`tsc` 与 mount smoke 都对着已安装的 dsh。`node scripts/link-dsh.mjs` 默认就是那份安装(`--runtime` 不带值时显式写同一个意思);只有 `--runtime

` 会换成另一份安装,那是 `check:old-dsh` 的跨版本门在用(在临时目录里装一份下限 dsh),不是日常构建的第二来源;`--no-bake` 只链接不写"编译对着谁"的记录。 client preset 的来源同样是固定的:仓库自带的 pin 住的副本 (`packages/plugin/vendor/dsh-client-preset/`,8 个文件 / 2334 行,逐字节拷贝;来源与重新对齐见 `ORIGIN.md`)。`tsdown.config.ts` 按字面相对路径 import 它,不经过任何 checkout 解析,也没有 env 开关。harness checkout 只被可选的漂移核对用到(`node scripts/check-preset-drift.mjs`),不参与编译。 只改了 UI 时用 `pnpm -C packages/plugin run bundle`(纯 tsdown,不做类型检查)。 #### 连续开发 ```bash pnpm -C packages/plugin run watch # tsdown --watch:持续重建 lib/index.js + lib/client.js(不做类型检查) ``` UI 改动边写边自动热替换;host 改动仍需重启 dsh。 (tsdown 的 client preset 按当前根 glob `packages/*/*/package.json` 找本插件的 manifest,所以 `link-dsh` 会在自带 preset 根下发布 stub `packages/client/avantf-dsh-mem/package.json`——指向 `packages/plugin/package.json` 的软链;stub 被删掉时重跑一次 `link-dsh` 或 `pnpm build:dsh` 即可。)