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 存储模型

单一 storageDomainmc_memory,三张表:

说明
memoriesid = hash(projectId·category·content)记忆行;幂等写的关键
notesid延迟意图便签
metakey配置(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 等)——机器来源文本永不进入记忆管线:

  1. 正则启发式:用户消息中的决策/约束句式(每条消息至多一条)。
  2. 配置写入write/edit 触碰知名配置文件名时记录变更摘要。
  3. 回顾通道(下节):能覆盖前两者原理上覆盖不了的隐式知识。

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 手删即可。
  • 存储域整快照持久化决定了写入必须串行:当前规模(≤数百条)没有性能问题, 若未来记忆量级增长需要重新评估存储后端。