Design
August 26, 2026 · View on GitHub
上游思想来源:https://github.com/cortexkit/magic-context (本项目是其记忆层思想在 DeepSeek Harness 上的原生重实现,未使用其任何代码) 运行时:DeepSeek Harness web profile(Cordis 组合;Service / Event / Slot 原语)
1. 定位与边界
dsh-hippocampus 只做一件事:给 DSH 会话一个跨会话、自我管理的项目记忆层 (捕获 → 巩固 → 召回)。
明确的非目标:
-
不接管上下文管理。压缩(compaction)、token 预算、工具结果裁剪归 DSH 所有; 本插件只以两个旁路钩子与其协作(见 §5),避免"双上下文管理器"冲突。
为何不效仿上游整体接管压缩——两层可行性的精确说明:这件事在 DSH 上分 两层。宿主组合层的全局替换不可行(官方 compaction 行挂载于各 preset 的 isolate realm,宿主面 provide 同名服务会被可见性过滤拦截,且重复 provide 直接抛错);预设层的换装则完全可行——用薄 preset include 官方组合树后
disabled官方行并insert自家行即可,社区存在走这条路线的移植实现。 我们不选择它,是两个权衡的结果:作用域——换装路线是 opt-in 的,行为只 存在于选择专用 preset 的会话,而记忆层要求对每个 preset 全局生效; 耦合深度——需要维护针对 DSH 内部结构(surface 派生、契约升级门、版本 基线)的整套适配。记忆层的用户价值不需要支付这笔成本:T1 以单一方法签名 的运行时接触面达到了同等的摘要增强效果,且天然覆盖全部预设。 -
不移植上游的缓存稳定变换(m[0]/m[1] 双槽、字节级回放)。该设计绑定特定 提示词缓存语义,与 DSH 的自动缓存底板不匹配。
-
不做语义检索之外的检索基础设施。向量就是普通 JSON 数组 + 进程内余弦—— 当前规模(数十至数百条记忆)不需要向量数据库。
2. 架构总览
2.1 进程布局
浏览器(DSH Web GUI)
└─ Magic Context 设置页(client.mjs:settings.section Slot)
│ 同源 fetch(仅 loopback 对端可写)
▼
宿主进程(index.mjs:Cordis 插件 apply(ctx))
├─ tools.register ctx_memory / ctx_search / ctx_note
├─ commands.register /ctx-status · /ctx-dream · /ctx-retro
├─ systemPrompt.section 记忆注入(每模型回合)
├─ ctx.on(session/event) 捕获 / 回顾缓冲 / 检查点落库 / 压缩前触发
├─ ctx.on(tools/result) 配置文件写入捕获
├─ webServer.register /mc-api/* 同源桥(设置 UI 的数据通道)
└─ storageDomain mc_memory 域(持久层)
两半均为纯 JS 函数体(无静态 import),由 sync.mjs 派生为可部署 ESM 并写入
profile 的本地包目录。派生时客户端半被包进 window.__ModuleLoader__.load({id, factory}) 经典脚本工厂(require("react") 经 dsh.client.external 声明)。
2.2 存储模型
单一 storageDomain 域 mc_memory,三张表:
| 表 | 键 | 说明 |
|---|---|---|
memories | id = hash(projectId·category·content) | 记忆行;幂等写的关键 |
notes | id | 延迟意图便签 |
meta | key | 配置(mc:cfg)、回顾游标(retro:<pid>)、嵌入向量(embv1:<id>) |
存储层按整快照读-改-写持久化——并发变更会互相复活。因此本插件的全部自动
写入都经过单条 promise 串行链(serialWrite);记录可能深冻结,更新一律合并
拷贝而非原地赋值。
2.3 Host ↔ Client 桥
harness.handle 仅存在于 @pluginId 动态插件沙箱,静态组合行不可用。设置页的
数据通道因此是宿主侧 webServer.register({ kind:'prefix', path:'/mc-api' }) +
浏览器同源 fetch。写路由只接受 loopback 对端;API Key 不出现在任何 GET 响应中。
3. 记忆管线
3.1 捕获
三个入口,全部经过 isMachineText() 内容分类器(运行时快照前缀、
<system-reminder>、检查点信封、注入块回显、markdown 头、自家子代理报告的
签名尾行 DREAM-DONE / RETRO-DONE 等)——机器来源文本永不进入记忆管线:
- 正则启发式:用户消息中的决策/约束句式(每条消息至多一条)。
- 配置写入:
write/edit触碰知名配置文件名时记录变更摘要。 - 回顾通道(下节):能覆盖前两者原理上覆盖不了的隐式知识。
3.2 回顾(retrospective)
对标上游同名任务的思想——用模型理解内容,而不是用正则猜内容:
$ 真人消息 → 内容分类 → 候选缓冲(\text{meta}, ≤30条 \times 600字) → [触发: 手动命令 / 维护\text{tick}(冷却30\text{m}) / \text{compaction}-\text{start}(强制)] → 纠正信号门(无信号 → 消费缓冲,零 \text{LLM} 成本退出) → 单个子代理(自持 \text{ctx\_memory} 写权,≤5 条,先对照现有记忆去重) → 幂等落库(\text{id} 即去重键,子代理失败也不产生半套状态) $
节流三件套:冷却退避(失败才计)、单飞锁、活动门(缓冲为空直接退出)。 背景触发的派发需要活的 agent 句柄作 subagent parent,缺失时良性跳过。
3.3 巩固(dreamer)
/ctx-dream 派发可继续子代理对照现有记忆做去重/整理,报告经运行时拼回会话。
one-shot 降级兜底;provider 偏好顺序 spawn → fork。
4. 召回管线
ctx_search 同时跑三条通道,分数合并排序(同分比 importance):
| 通道 | 得分 |
|---|---|
| 关键词 | `Intl.Segmenter('zh')$ 词元重叠 \times 10 + 词元覆盖率 \times 20 |
| 子串强命中 | 整句 \text{verbatim} 命中 \times 50(保证旧行为不回归的地板分) |
| 语义(可选) | \text{cosine}(\text{queryVec}, \text{rowVec}) \times 40;任一方向不可用即整体缺席 |
语义通道的向量存于 \text{meta} $embv1:={h, v, m}: h 是内容哈希(编辑即失效)、m是产出模型标识(换模型即失效)——双守卫 任一失配视为过期,由维护 tick 重嵌。支持两种 wire 方言:Ollama{model, prompt}与 OpenAI 兼容{model, input:[...]}(含 Bearer Key)。 请求层携带 keep_alive: 30m`,避免稀疏使用反复支付冷加载。
超时按场景拆分:查询 8s(要快)/ 探测 60s / 清扫 120s(容忍首次冷加载)。 提供方不可达 → 探测标记 unavailable + 10 分钟冷却 → 一切行为与纯关键词模式 逐字节一致。
5. 与原生压缩的协作
所有权不动:压缩决策、历史替换、token 预算全部归 DSH。本插件挂两个旁路钩子:
- T0(输出侧观测):
compaction/summary事件把摘要抄送一份进 mc_memory, 存为 CHECKPOINT 分类——可检索、永不注入提示词(防止回声循环)。 - T1(输入侧增强):守卫式包装
BasicCompactionEngine.prototype.summarize, 在重放消息之后、固定压缩指令之前追加一条记忆清单消息(指令必须保持最后一条 user message 的不变式)。三重守卫任一不满足则静默回退原方法;CHECKPOINT 刻意 不参与注入,同样是为了断开回声路径。
compaction/start 同时是回顾管线的强制触发位(对应上游 historian 在压缩边界的
事实晋升时机)。
6. 配置模型
持久化于 meta mc:cfg,读取时合并默认值(类型校验,未知键忽略),改动即时生效:
{
embedEnabled, embedProvider /* 'ollama'|'openai' */,
embedEndpoint, embedModel, embedApiKey,
captureEnabled, retroEnabled,
injectLimit /* 1–20 */, injectMaxChars /* 200–8000 */
}
写入入口是 /mc-api/settings;所有消费方每轮调用 getCfg() 取合并视图。
7. DSH 静态插件实现备忘
供后续开发同类插件的参考(均经实证):
- 客户端 bundle 契约:
dsh-client-modules把包的exports["./client"]以经典 script 注入浏览器;必须注册window.__ModuleLoader__.load({id, factory}), React 通过dsh.client.external: ["react"]+ 工厂内require("react")获取。 ESM 语法的export在经典脚本里是 SyntaxError,且加载事件照常触发——报错形态 是 "bundle loaded without registering"。 harness.handle是动态插件专用:静态组合行的宿主半没有 harness 全局; Client↔Host 自定义通信走webServer.register路由 + 同源 fetch。- 服务可见性有激活时序:无依赖边的并行激活下,先激活的行
ctx.get后激活行 的服务得到 undefined(get 不等待,inject 才等待)——消费基础服务必须声明进inject。 - storageDomain 记录可能深冻结:严格模式下原地赋值抛 TypeError,一律合并拷贝。
startContinuable无条件读signal.aborted:signal 必须始终提供;后台触发 源需自备 AbortSignal。- 打包脚本的清单规范化要"补齐缺失"而非整体替换,否则会悄悄丢弃新增的硬依赖。
8. 限制与降级
- 语义检索每次查询增加一次嵌入调用(bge-m3 CPU 热推理约 6s;
keep_alive已将 冷加载摊薄为 30 分钟一次)。查询路径 8s 超时——若恰逢冷载会放弃语义增量, 该轮退化为关键词结果。 - 跨会话历史搜索需要部署层启用
session-query-sqlite(官方默认 opt-in)。 - 子代理报告经签名+结构过滤后仍存在新型散文报告的理论漏检——发现漏网记忆行
用
ctx_memory delete手删即可。 - 存储域整快照持久化决定了写入必须串行:当前规模(≤数百条)没有性能问题, 若未来记忆量级增长需要重新评估存储后端。