Files
HologramAgent-DSH-Memory-Port/docs/feasibility.md
jianf 28d9d5e929 docs: 三层记忆移植 dsh 插件可行性评估
结论:只能做子集。

逐层结论
- L1 (context 热上下文) ≈20%:dsh 的上下文压缩是「摘要替换区间」
  (dsh-compaction-basic 的 compactSurfaceRegion),不是「按 query 相关性
  淘汰 + 迁出到冷层 + 按原时间戳迁回」。CompactionTrigger 接口拿不到
  query;MessageBase 无 timestamp 字段,surface 是 append-only + replace,
  无法语义插入。
- L2 (coldcontext 冷文档) ≈80%:存储/召回/注入可落,缺
  「Peek → 回灌 → Remove」的回收站语义。
- L3 (stablemem 图记忆) ≈85%:memory_blocks/memory_block_edges 结构
  完全可映射,node:sqlite 实测可用(含 FTS5);受限于无 embedding 服务。

关键发现
- dsh 无任何 embedding/vector service,需自建(推荐复用 HA 的 http
  provider HTTP 协议转发,2 人天)。
- systemPrompt.context() 的 AssembleContext 拿不到本步消息正文,
  「按本次输入召回后注入」必须走 agent/pre-step waterfall
  (参考 dsh-time-context 的用法)。
- dsh attachment 是 CAS,与 HA「媒体字节内联在块里、不做引用账本」
  的设计冲突 → 媒体块必须裁掉。

工作量(含判据与实测)
- L2+L3 子集:36-53 人天
- MVP(L3 + 工具 + 注入面):19-25 人天
- 完全等价(含迁出-迁回):不可行,不报价

报告含 7 章 + 两份附录(HA/dsh 双侧证据索引、环境实测结果)。
原文全程只读,未修改 HologramAgent 仓库。
2026-10-30 00:39:44 +08:00

1139 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 可行性评估:把 HologramAgent 三层记忆系统移植为 dsh 插件
> 状态:**调研评估,不含实现**
> 评估对象:HologramAgent 内核 `internal/memory/` + `internal/agent/core/` 的三层记忆
> 目标宿主:dsh(deepseek-harness)`0.2.1-rc.2`,cordis 依赖注入内核,插件形态为导出 `apply(ctx, config)`
> 取证方式:只读源码 + `lib/*.d.ts` 权威类型。**本文每条断言都标注了文件路径与符号名**,
> 无标注的推断一律显式写「推断」。
---
## 0. 结论先行
| 结论四选一 | **只能做子集** |
|---|---|
| 一句话理由 | L2 与 L3 能落到 dsh 的真实扩展点上(`systemPrompt.context` 注入 + `ctx.tools.register` 工具面 + 自建 `Service` 做检索);**L1 的「按 query 相关性淘汰页、把整帧迁出、再按原始时间戳迁回」在 dsh 里没有对应钩子**——dsh 的上下文压缩是「摘要替换区间」(`dsh-compaction-basic` 的 `compactSurfaceRegion`),不是「迁移低相关帧到冷层再按需迁回」,语义相反且接口签名里拿不到 query。 |
| 分层可移植性 | L3 ≈ 85%(结构完全可映射,受限于向量依赖)<br>L2 ≈ 80%(存储与召回可映射,缺「回收站式 Consume + 迁回原时序」)<br>L1 ≈ 20%(只有「进 prompt 的注入点」可挂,淘汰/迁移机制挂不上) |
| 可整体搬运的资产 | **`Block` 数据结构本体**(`memory.MemoryBlock`)+ 确定性 ID 派生 + 仲裁/拒答/符号检索算法。这三块与宿主无关,是移植成本最低、收益最高的部分。 |
| 硬阻塞(不可绕过) | ① dsh 无 embedding 服务;② dsh 消息是无时间戳的、只能追加的 surface,无法「按原始时间戳把事件插回时序」;③ dsh 无「迁出-迁回」的双层持有模型 |
| 工作量粗估 | **L2+L3 子集(含判据)36–53 人天**;压到 MVP(L3 图记忆 + 工具 + 注入面,不含 L2/仲裁/拒答)**19–25 人天**;含 L1 降级版(`form:'recall'` 追加,不做淘汰)再加 8–12 人天。**「完全等价带迁出-迁回」不可行**,不建议报价。详见 §6.2。 |
> 工作量口径:§6.2 的分阶段表是**含判据与实测**的报价口径(请以此为准);
> 早前草稿里的 28–40 人天是不含判据、不含工具 `output` 声明的乐观值,已废弃。
---
## 1. L1 / L2 / L3 的核心数据结构与数据流,最小可移植单元
### 1.1 三层的统一载体:`MemoryBlock`
**权威定义**:`/home/program/TrueAgent/internal/memory/block.go:66` `type MemoryBlock struct`
```go
type MemoryBlock struct {
ID string // 内容派生(确定性)
Modality BlockModality // text | image | video | audio
Text string // 文本块负载(union 的一半)
Data []byte // 媒体块负载(union 的另一半)
MIME string
Size int
Width, Height int
Vector []float64
Fingerprint string // 向量空间指纹
Source, Tool, Scene string
SemanticType string
// ── 段页式身份(2026-10-29)──
FrameTS time.Time // 帧号 = 时间戳,不另造自增号
PageSeq int // 帧内页序号(1..n)
PageKind PageKind // llm_round | tool_round
BlockRole BlockRole // request | response
CallID string // tool_round 的配对键
Inactive bool // 本次渲染不参与(标注,非删除)
LastRecalledAt time.Time
CreatedAt, UpdatedAt time.Time
}
```
关键设计约束(原文注释,`block.go:55-56` 与 `62-65`):
- 块**不携带 Layer / Owner / RefCount**:「块当前由哪个层的容器持有,哪个层就是唯一事实源。
Context→Document→Graph 迁移的是这个值本身,不建立平行保活账本。」
- 负载是 **union**:文本块负载在 `Text`,媒体块在 `Data`。「不存在『块 + 外挂字节层』。」
- `FrameTS` 是帧号,**不跨帧**(`block.go:100-125`):同一 `FrameTS` 的块整批待在同一层,
因为「帧是最小可渲染单元,拆开就会出现『有 call 没 result』的破帧」。
**最小可移植单元 = `MemoryBlock` 这个纯数据结构本身**(一个 JSON 可序列化的值对象)。
它没有任何 Go 接口依赖,可以直接翻译成 TypeScript interface。这是整个移植里
唯一「零成本」的部分。
### 1.2 L1 = Context 热上下文
**位置**:`/home/program/TrueAgent/internal/agent/core/context.go`
| 符号 | 行 | 职责 |
|---|---|---|
| `type ContextEvent struct` | 64 | 一条事件:`Timestamp/Source/Input/Response/ToolsUsed/ToolResults/ToolCalls/Blocks/NoMemory/InactivePages` |
| `type RelevanceContext struct` | 145 | 持有 `events []*ContextEvent` |
| `(*RelevanceContext).Append` | 967 | 唯一入口:`scrubEventSecrets` → `computeVector` → 追加 → `claimPendingToolResults` → `save()` |
| `(*RelevanceContext).Prune` / `PruneWithProtected` | 1059 / 1075 | 按预算裁剪,把低分事件归档到 L2 |
| `(*RelevanceContext).InsertByTimestamp` | 998 | 召回时按原始时间戳插回 |
| `type ColdRecallPlan struct` | 453 | `planColdRecall` 的产物:超预算时按页相似度标 `inactive` |
**数据流(`Prune` 的真实实现,`context.go:1075-1210`(函数体至 :1207))**:
```go
func (c *RelevanceContext) PruneWithProtected(currentInput string, topK int,
coldcontextStore *coldcontext.Store, pCount int) int {
c.resetInactivePagesLocked() // 剔除标记是渲染期状态,每次重置
if len(c.events) <= topK { return 0 }
protected := c.events[len(c.events)-pCount:] // 最近 N 条无条件保护
candidates := c.events[:len(c.events)-pCount]
// ① 用当前 query 给每条候选事件打相关性分
for i, evt := range candidates {
if useDense && evt.DenseFP == queryFP { // 只比同指纹向量
score = vector.DenseCosine(queryDense, evt.DenseVec)
} else {
score = vector.CosineSimilarity(queryVec, evt.Vector) // 退化稀疏
}
}
// ② 保留 topK,其余 archive
keep := scoredEvents[:keepCount]
archive := scoredEvents[keepCount:]
// ③ archive 的**整条事件(连同它持有的块)**迁往 L2
doc, _ := coldcontextStore.ContextToColdContext("context_archived", entries, ...)
// ④ 迁移语义:宿主侧清空块,确保同一块不同时留在两层
for _, s := range archive { s.event.Blocks = nil }
}
```
L1 的三个不可替代特征:
1. **事件有 `Timestamp`**,且 `InsertByTimestamp` 允许**乱序插入到历史位置**——这是「召回 =
把事件迁回原时序」的前提。
2. **按「页」淘汰**(`InactivePages []int`,`json:"-"`,「不持久化、不迁移」),
而不是按「消息」淘汰——因为 `assistant(tool_calls)` 后必须紧跟配对的 `tool` 消息。
3. **淘汰是「迁移」不是「摘要」**:`Prune` 把整条事件原样(正文不截断)交给
`ContextToColdContext`,L1 里删掉、L2 里完整存在。
### 1.3 L2 = coldcontext 冷文档
**位置**:`/home/program/TrueAgent/internal/memory/coldcontext/coldcontext.go`
| 符号 | 行 | 职责 |
|---|---|---|
| `type ColdDoc struct` | 37 | 文档 = 检索元数据 + `Events []ColdEvent` |
| `type ColdEvent struct` | 75 | 与 `ContextEvent` 同构;**正文不在字段里,在 `Blocks` 里** |
| `(*Store).ContextToColdContext` | 385 | L1→L2 迁移的写法 |
| `(*Store).Peek` / `Consume` / `Query` | 480 / 526 / 537 | 读(只读 / 读+删 / 只读) |
| `(*Store).QueryScored` / `denseSearchScored` | 556 / 616 | 带分数的向量召回 |
| `docBlocksForEntry` | 974 | 把一条 `ContextEntry` 拆成 request/response/tool 块,**填 FrameTS/PageSeq/PageKind/BlockRole/CallID** |
| `(*ColdEvent).InOut` | 1259 | 从块读回 `(input, response, held)` —— 召回时的对称解读 |
**关键点**:`ColdDoc.Content` 只是**渲染视图**(`coldcontext.go:33-36` 注释),
原文在 `Events[i].Blocks` 里文本块的 `Text`。这是「L1/L2 同构」的落地方式。
**流转方向(`ARCHITECTURE.md:200-230`)**:
```
L1 --Prune 归档(迁移)--> L2
L2 --coldcontext_query(迁回)--> L1 ※ 块 ID / 向量 / 时间戳全程不变
L2 --FindColdContexts(72h, ≤2次) 冷化--> L3
```
### 1.4 L3 = stablemem 图记忆
**位置**:`/home/program/TrueAgent/internal/memory/block.go` + `graph.go`
存储 schema(`graph.go:2577` `ddlMemoryBlocks`):
```sql
CREATE TABLE memory_blocks (
id TEXT PRIMARY KEY, modality TEXT NOT NULL, text_content TEXT,
data BLOB, mime TEXT, size INTEGER, width INTEGER, height INTEGER,
vector TEXT, -- JSON 编码的 []float64
fingerprint TEXT, source TEXT, tool TEXT, scene TEXT, semantic_type TEXT,
frame_ts TIMESTAMP, page_seq INTEGER, page_kind TEXT, block_role TEXT,
call_id TEXT, inactive INTEGER, last_recalled_at TIMESTAMP,
created_at, updated_at
)
```
```sql
CREATE TABLE memory_block_edges (
id, source_kind, source_id, target_kind, target_id, type,
confidence, session_id, turn_id, status, merged_into, created_at
)
```
| 符号 | 行 | 职责 |
|---|---|---|
| `(*GraphDB).PutMemoryBlocks` | block.go:203 | 事务写入,`ON CONFLICT` 全部走「空值不覆盖」策略 |
| `(*GraphDB).AddMemoryBlockEdge` | block.go:497 | 端点存在性校验 + 唯一性 |
| `(*GraphDB).RecallBlocks` | block_recall.go:78 | 全表扫向量 + 余弦 + 指纹/维度跳过 + 模态过滤下沉 SQL |
| `(*GraphDB).RecallBlocksWithArbitration` | block_recall.go:229 | 放大 TopK → 仲裁 → 截断 |
| `(*GraphDB).RecallBlocksGuarded` | abstain.go:179 | 拒答闸门 → 召回 |
| `arbitrate` | arbitration.go:375 | 同属性多值的时序仲裁 |
| `AbstainCheck` | abstain.go:98 | 符号零命中 ⇒ 拒答 |
| `QuerySymbols` | fusion.go:145 | jieba 分词 + 数字/Latin 正则 → 符号集 |
| `LoadCentroid` / `centerBoth` | centroid_store.go:144 / 443 | 各向异性校正(去均值) |
| `(*GraphDB).RecallByScene` | scene.go:344 | 场景前缀匹配召回 |
**L3 的 5 条召回链路**(全部读 `memory_blocks`,同一份数据五种取法):
1. **向量路**:`RecallBlocks` → 余弦,带 fingerprint/dim 跳过
2. **符号路**:`QuerySymbols` → `exactHit` 布尔置顶(`fusion.go:427` `Score = 1.0 + VectorHit*0.01`)
3. **融合**:`RecallBlocksFused` → `0.7×vector + 0.3×symbol`,精确串单独置顶
4. **仲裁**:`arbitrate` → 同属性取最新(用 `parseFact` + `supersedes`)
5. **拒答**:`AbstainCheck` → 符号零命中且含数字串 ⇒ 拒答
**重要实测边界**(`docs/zh/recall-capability-tiers.md`):纯泛化描述(「公网地址是什么」)
目标块排名 389/2686;纯中文「库里没有」无法检测,会编造。移植这些**缺陷也一起移植**。
### 1.5 最小可移植单元汇总
| 层 | 最小可移植单元 | 依赖 | 难度 |
|---|---|---|---|
| 共同 | `MemoryBlock` 值对象 + 确定性 ID 派生(`distill.BlockID` = `sha256(sentence␀subject␀dim␀value)[:12]`) | 无 | 极低 |
| L1 | `ContextEvent` + 「按页淘汰 + 整帧迁移」策略 | 需要宿主提供「可淘汰的上下文」抽象 | **高(dsh 没有)** |
| L2 | `ColdDoc{Events[]}` + `docBlocksForEntry`/`InOut` 的对称拆装 | 需要一个 KV/文件存储 + 向量 | 中 |
| L3 | `memory_blocks` + `memory_block_edges` + 5 条召回链路 | 需要 SQLite + 向量 + 分词 | 中高 |
---
## 2. dsh 的扩展点盘点:哪些能挂记忆、哪些挂不上
dsh 是 cordis 依赖注入内核。插件形态:
```ts
// /usr/lib/node_modules/@deepseek-ai/dsh-storage-json/lib/types/index.d.ts
export declare function apply(ctx: Context, config: Config): void;
```
`ctx` 是 cordis 代理,服务通过 `Service` 基类构造时注册
(`cordis/lib/types/service.d.ts:38` `constructor(ctx, name)` →
`ctx.reflect.provide(name, this, check)`),随 fiber 卸载自动注销。
### 2.1 service 注册
**位置**:`cordis/lib/types/service.d.ts` + `cordis/lib/types/context.d.ts`
```ts
abstract class Service<out T = never> {
constructor(ctx: Context, name: string); // 立即注册,随 fiber 卸载
[symbols.resolveConfig](base?, head?): T; // 祖先 intercept 配置合并
}
// Context 上还有:
ctx.isolate(name, label?) // 独立服务作用域
ctx.intercept(name, config) // 按插件注入配置
ctx.set(name, value) // 直接写
```
**能挂记忆**:✔ **这是最关键的可挂点**。L3 图记忆可以做成
`class StableMem extends Service` → `super(ctx, 'stablemem')`,
下游通过 `ctx.stablemem.recall(...)` 使用。dsh 自己的
`ctx.tools` / `ctx.sessions` / `ctx.compaction` / `ctx.attachments` / `ctx.storage` /
`ctx.systemPrompt` 全部是这个模式。
**挂不上**:无。这是通用机制。
### 2.2 agent 生命周期钩子
**位置**:`dsh-agent/lib/types/runtime-types.d.ts`(`declare module '@deepseek-ai/cordis' { interface Events { ... } }`)
| 事件 | mode | 参数 | 对记忆的用处 |
|---|---|---|---|
| `agent/created` | serial | `{agent, source, signal}` | **初始化本 session 的记忆命名空间**;`source: 'startup'\|'resume'\|'clear'\|'compact'` 可直接决定「这是新会话还是恢复」 |
| `agent/disposed` | emit | `{agent}` | 冲刷未落盘的块 |
| `agent/status` | emit | `{agent, status: 'idle'\|'running'}` | 只在 idle 时跑蒸馏(对应 HA 的 `distillLoop`) |
| `agent/inbox/inserted` | emit | `{agent, message}` | 用户输入到达 → 预填充召回的触发点 |
| `agent/inbox/claimed` | emit | `{agent, message, turn}` | 输入被消费进 step |
| `agent/pre-step` | **waterfall** | `{agent, messages, turn, step, signal}` → `PreStepDecision` | **★ 可替换本步进入模型的消息列表**(`{kind:'enter', messages}`)。这是唯一能「改写本次请求内容」的钩子 |
| `agent/request` | waterfall | `{...}` → `LlmCallConfig` | 只能换 provider/model,**不能改消息**(注释明确:「this waterfall cannot mutate messages」) |
| `agent/turn-stopping` | serial | `{agent, turn, signal}` | 轮末:可 `agent.steer(...)` 插入新内容,或做维护 |
| `agent/assistant-stream` | emit | `{agent, frame}` | 只读观测,可做流式蒸馏输入 |
| `agent/error` / `agent/request-error` | emit / waterfall | — | 错误恢复,与记忆无关 |
**能挂记忆**:✔ `agent/pre-step`(改本步消息)、`agent/turn-stopping`(轮末蒸馏)、
`agent/created`(命名空间初始化)。
**挂不上**:✘ **没有「上下文即将超限、请把低相关事件迁出」的钩子**。
dsh 把这件事完全交给 compaction 后端(见 2.4)。
### 2.3 context 组装点
**位置**:`dsh-system-prompt/lib/types/index.d.ts`
```ts
class SystemPrompt extends Service {
section(section: PromptSection): () => void; // 静态 system prompt 段
context(context: PromptContext): () => void; // ★ 动态上下文,落成 durable user-role 快照
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void;
variable(name, provider): () => void;
assemble(context?: AssembleContext): Promise<PromptAssembly>;
}
interface PromptContext {
name: string; // 唯一名,重复注册抛错
order: number; // 升序拼接
text: string | ((context: AssembleContext) => string); // ★ 每次组装求值
required?: boolean;
interpolate?: boolean;
}
// AssembleContext(:37-44)—— ★ 基定义只有两个字段;agent 由 dsh-agent declaration merging 扩展
interface AssembleContext {
scope?: ScopeKey;
signal?: AbortSignal;
// agent?: Agent; ← 由 `dsh-agent/lib/types/runtime-types.d.ts`
// `declare module '@deepseek-ai/dsh-system-prompt' { interface AssembleContext }` 合并进来
}
```
`CONTEXT_ORDERS` 已经分配了槽位(`index.d.ts:155-163`):
`WORKING_DIRECTORY: 100` / `SANDBOX_POLICY: 110` / `APPROVAL_POLICY: 115` /
`SUBAGENT_DELEGATION: 120` / `TEAM_IDENTITY: 125` / `HARNESS_SOURCE: 200` / `WEB_SURFACE: 210`。
另有瀑布式总装口:
```ts
'system-prompt/assemble'(this: Scoped<SystemPrompt>,
assembly: PromptAssembly, context: AssembleContext,
next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
```
**能挂记忆**:✔✔ **这是常驻内容的最佳落点,但不是「按本次输入召回」的落点**。
必须区分两件事(★ 实测核对源码后修正了初稿的判断):
| 需求 | 钩子 | 依据 |
|---|---|---|
| 「与本次输入无关的常驻段」(记忆总量统计、已存块数、维护提示) | `systemPrompt.context()` | 用法见 `dsh-working-directory/lib/index.js:41-47` |
| **「用本次输入做召回,把结果注入本轮」** | **`agent/pre-step` waterfall** | 用法见 `dsh-time-context/lib/index.js:218-243` |
**为什么不能用 `context()` 做召回**:`PromptContext.text(context)` 拿到的
`AssembleContext` 的基定义只有 `{scope, signal}`(`dsh-system-prompt/lib/types/index.d.ts:37-44`),
`agent` 是 `dsh-agent` 通过 declaration merging 后加的(`runtime-types.d.ts` 的
`declare module '@deepseek-ai/dsh-system-prompt'`)——**两者都没有本步消息正文**,
**没有本步消息正文**。而 HA 的 `crossModalMarkdown` 需要 query
(`internal/agent/core/tooldefs.go:51` 传的是 `input`)。
所以 `agent/pre-step` 才是对等物:
```ts
// dsh-time-context/lib/index.js:218 —— 真实用法(已核对)
ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next) => {
const decision = await next()
if (decision.kind === 'reject' || signal.aborted) return decision
// ... 拼 text ...
return {
...decision,
messages: [...decision.messages, createUserMessage({
content: [{ type: 'text', text }],
source: { kind: name, form: 'snapshot', sections: [{ name, text }] },
})],
}
}, { prepend: true })
```
**挂不上**:✘ `section` / `context` 都是**追加**,无法**移除**已有内容。
所以「把超限事件从上下文里移出」不可能通过这个扩展点做到。
**另一个有用的发现**:`ctx.sessionProjections`(`dsh-session-projection/lib/types/index.d.ts:25`)
提供**纯同步 fold**(`ProjectionDefinition.apply(state, event)`,
「All functions MUST be synchronous」),并自带订阅、水位与变更通知。
很适合用来维护「本 session 已写过哪些块的 ID」「当前向量空间 fingerprint」这类
派生状态——比自己在 `session/event` 上手工累加安全。`dsh-time-context` 就是用它
记录 `lastMessageTime` / `lastInjectionTime`。
同时注意一个硬约束(`dsh-system-prompt/lib/types/index.d.ts:14-27` 的 `system-prompt/assemble` 注释):
> A registered complete section is restored after this waterfall,
> so listeners cannot add to or replace that scope's system prompt.
即如果有 `complete: true` 的 section 被注册,瀑布层的增补会被丢弃。
### 2.4 compaction
**位置**:`dsh-compaction/lib/types/index.d.ts` + `dsh-compaction-basic/lib/types/region.d.ts`
```ts
abstract class CompactionEngine extends Service {
abstract compactIfNeeded(agent: CompactionAgentContext,
trigger: 'pressure' | 'context-overflow',
signal: AbortSignal): Promise<CompactionResult | null>;
abstract compactNow(...): Promise<CompactionResult | null>;
abstract compactRegion(start: SessionSeq, end: SessionSeq, ...): Promise<CompactionResult>;
}
```
实际执行者(`region.d.ts`):
```ts
export declare function compactSurfaceRegion(
dependencies: RegionDependencies, session: Session,
start: SessionSeq, end: SessionSeq, agent: Agent,
options: CompactionTransactionOptions, signal?: AbortSignal
): Promise<CompactionResult>;
```
注释里写清了它做的事(`region.d.ts:40-46`):
> Resolve the next range starting at the first non-system surface node while
> retaining a priced recent tail and **never splitting an assistant
> tool-call/result pair**. […] A successful run **replaces the selected surface
> span with one summary node** and prevents concurrent compaction of the same session.
**★ 这是 L1 移植的核心阻塞点**,必须逐条对比:
| | HA 的 `Prune` | dsh 的 `compactSurfaceRegion` |
|---|---|---|
| 裁掉的去向 | **迁移**到 L2,原文完整保留,可召回 | **替换成一段 LLM 摘要**,原文留在日志里但**不在 surface 上** |
| 可逆性 | 可逆(`coldcontext_query` 迁回原时序) | 模型视角不可逆(要回滚只能看原始日志重建) |
| 评分依据 | 当前 query 的向量余弦 | `ctx.tokenMeter` 的压力测量 + retention ratio,**与 query 无关** |
| 保留策略 | 最近 `protectedCount` 条 + 相关度 topK | `retainRatio`(默认 0.16)× 窗口的尾部预算 |
| 摘要方式 | **不摘要**(原文进 L2) | 一次额外 LLM 调用(`summarize()`,`BasicCompactionEngine` 唯一可覆写钩子) |
| 页边界 | 按页淘汰(`InactivePages`) | 「never splitting an assistant tool-call/result pair」(`selectCompactableRange`) |
**能挂记忆**:△ 有第二个 compaction 后端 `dsh-compaction-tool-result-pruner`
(`pruneSession(session)`,确定性 head/middle/tail 裁剪,**不调模型**),
这证明「不调模型的压缩」是可行的。但它的产物仍是**替换 surface 节点**,
不是「迁出到另一个层」。
**挂不上**:✘ **无法实现「按 query 相关性做淘汰决策」**。
`CompactionEngine` 的接口签名里没有 query,`CompactionTrigger` 只有
`'pressure' | 'context-overflow'` 两个值。要做 query 相关的淘汰,
只能绕过 compaction 服务自己改 surface——而 `session.append` 是 append-only,
没有「删除事件」的 API(见 2.5)。
### 2.5 session log / surface
**位置**:`dsh-session/lib/types/index.d.ts` + `surface.d.ts`
```ts
class Session {
get surface(): SessionSurface { readonly nodes: readonly SessionSeq[]; ... }
append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts): SessionEvent<T>;
deriveMessages(): Message[]; // 行 347
}
interface SessionSurface {
readonly nodes: readonly SessionSeq[]; // 模型可见的事件序
readonly replaceGeneration: number;
readonly contentGeneration: number;
}
```
注释明确(`index.d.ts:1-5`):
> Event-sourced session service: **append-only** session log, in-memory store,
> and the derived LLM message history.
surface 是「事件日志之上的一层有序视图」(`surface.d.ts:1-6`):
「The append-only log remains the source of truth.」
**能挂记忆**:△ `session/event` 事件(emit)可以 tap 到每一条追加的事件
→ 这可以做**增量蒸馏输入**(等价 HA 的 `pipeline.Append`)。
`session/flush`(parallel)可以做**落盘检查点**。
**挂不上**:✘✘ **这是第二个硬阻塞**:
1. **消息没有时间戳**。`MessageBase`(`dsh-llm/lib/types/message.d.ts:124-136`)只有
`id / content / source`,**没有 `timestamp`**。HA 的「按原始时间戳把事件插回时序」
(`InsertByTimestamp`)在 dsh 里无对应物——surface 是**有序数组**,
`replace` 只能替换**已存在的某个位置**,不能「插到历史中间」。
2. **没有「消费式删除」**。HA 的 `coldcontext_query` 是 `Peek → 回灌 → Remove`
(`toolcall.go:1109-1190`),迁移语义。dsh 的 `session.append` 只能追加,
surface 只能 `replace` 一个位置为别的消息,不能「移出一批事件到另一个存储」。
3. **没有 `FrameTS`/`PageSeq` 概念**。dsh 用 `turn` / `step`(`'turn/start'`, `'step/start'`,
`dsh-agent/lib/types/types.d.ts` `TurnBoundaryProjection`)作为边界。
两者可以映射(**推断**:`turn` ≈ `FrameTS`,`step` ≈ `PageSeq`),
但 dsh 的边界是**单调递增的整数**,不是时间戳,且**不参与块的身份**。
### 2.6 attachment(媒体块)
**位置**:`dsh-attachment/lib/types/index.d.ts`
```ts
abstract class AttachmentStore extends Service {
abstract saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>;
abstract readImage(ref: ImageAttachmentRef, signal?): Promise<StoredImageAttachment>;
saveFile(input: SaveFileAttachment): Promise<FileAttachmentRef>; // 内容寻址、逐字节
readFileStream(ref, signal?): AsyncIterable<Uint8Array>;
admitPromptContent(content): Promise<AdmittedPromptContentPart[]>;
}
```
对比 HA 的媒体块(`block.go:62-65` 与 `data` 字段注释):
> L0/L1 的块内联在 JSON 里(Data 序列化为 base64),L3 的块落 SQLite 的 BLOB 列。
> 无论哪层,字节都不脱离块单独存在。
**能挂记忆**:△ **部分可挂,但语义不同**:
| | HA 媒体块 | dsh attachment |
|---|---|---|
| 字节位置 | **随块**(块外无字节存储) | **块外**,`AttachmentRef` 是引用(content-addressed) |
| 生命周期 | 随块同生同灭 | 独立(「already published content-addressed objects may stay unreachable until a future retention policy collects them」) |
| 引用账本 | **刻意不做**(`block.go:62-65`:「会凭空多出一层…引用账本…并与『块是唯一记忆载体』直接冲突」) | 没有账本,靠 GC |
**挂不上**:✘ HA 明确否决过 CAS 形态(`block.go:62-65`),
而 dsh attachment **就是 CAS**。移植时若用 attachment 存媒体块,
就必然引入 HA 刻意避免的「第二套生命周期」。反向选择是「把 `Data` base64 进 KV」,
代价是 `ctx.storage` 的 JSON backend 会把大 blob 序列化进文档
(`dsh-storage-json/lib/types/index.d.ts`:「one human-readable document per unit」)。
**结论**:媒体块是**必须裁剪**的部分(详见 §5 降级方案)。
### 2.7 tool 注册
**位置**:`dsh-tools/lib/types/index.d.ts`
```ts
class ToolRuntime extends Service {
register(definition: ToolDefinition): () => void; // 行 637
restrict(filter: ToolRestriction): () => void; // 行 654
schemas(scope?: ScopeKey): ToolSchema[]; // 行 712
}
interface ToolDefinition extends ToolSchema {
readonly output: ToolOutputDefinition; // ★ 强制:schema + render
execute(args: unknown, exec: ToolRunContext): Promise<unknown>;
timeoutMs?: number;
isConcurrencySafe?(args: unknown): boolean;
presentCall?(args): ToolCallView | undefined;
presentResult?(args, result): ToolResultView | undefined;
}
```
工具管线事件(`declare module '@deepseek-ai/cordis' { interface Events { ... } }`):
- `tools/pre-execute`(waterfall)→ `PreToolDecision`:allow / deny / cancel / ask
- `tools/execute`(waterfall):超时、重试、指标
- `tools/post-execute`(waterfall)→ `PostToolDecision`:**接受/替换/阻断结果**
- `tools/result`(emit):只读观测最终结果
- `tools/change`(emit,**未做 scope 过滤**):工具集变化
**能挂记忆**:✔✔ **这是最完整可挂的扩展点**。HA 的
`stablemem_*` / `coldcontext_*` 工具面(`internal/agent/core/tooldefs.go:610-700`)
可以逐个 1:1 翻成 `ToolDefinition`。`tools/post-execute` 还能实现
HA 的「工具输出触发的自动召回」(`memoryPass`)。
**挂不上**:无。**注意一个额外成本**:`ToolDefinition.output` 是**强制**的
(`output.schema` + `output.render`),每个工具都要写一份 JSON Schema 与 render 函数。
HA 的工具只有 `description` + `parameters`(`toolDefWith(...)`)。
### 2.8 汇总表
| 扩展点 | 位置 | L1 | L2 | L3 | 备注 |
|---|---|---|---|---|---|
| `Service` 注册 / `ctx.<name>` | `cordis/service.d.ts:38` | — | ✔ 存储 | ✔✔ 召回服务 | 通用机制 |
| `agent/pre-step` (waterfall) | `dsh-agent/runtime-types.d.ts` | ✔ 注入 | ✔ 注入 | ✔ 注入 | 可替换本步消息 |
| `agent/turn-stopping` (serial) | 同上 | ✘ | △ 蒸馏触发 | ✔ 蒸馏触发 | |
| `agent/created` / `disposed` | 同上 | △ | ✔ 命名空间 | ✔ 命名空间 | `source` 区分 startup/resume |
| `session/event` (emit) | `dsh-session/index.d.ts` | △ tap 增量 | ✔ 归档输入 | ✔ 蒸馏输入 | |
| `systemPrompt.context()` | `dsh-system-prompt/index.d.ts:269` | △ 常驻段 | △ | ✔ 记忆统计 | 纯追加,不能移除;**拿不到本步输入** |
| `agent/pre-step` + `createUserMessage` | `dsh-time-context/lib/index.js:218` | ✔✔ **召回注入** | ✔✔ 自动预填充 | ✔ 记忆索引 | `AssembleContext` 无输入文本的替代方案 |
| `ctx.sessionProjections` | `dsh-session-projection/index.d.ts:25` | △ | ✔ 派生状态 | ✔ 派生状态 | 纯同步 fold,自带水位 |
| `system-prompt/assemble` (waterfall) | 同上 :27 | △ | △ | △ | 有 `complete` section 时失效 |
| `ctx.compaction` 子类 | `dsh-compaction/index.d.ts` | ✘ **语义相反** | ✘ | ✘ | 摘要替换 ≠ 迁移 |
| `ctx.toolResultPruner` | `dsh-compaction-tool-result-pruner` | △ 可参考 | ✘ | ✘ | 不调模型但仍是 replace |
| `ctx.attachments` | `dsh-attachment/index.d.ts` | ✘ CAS 冲突 | ✘ | ✘ | 违反 HA「不做引用账本」 |
| `ctx.tools.register` | `dsh-tools/index.d.ts:637` | ✔ | ✔✔ | ✔✔ | 需补 `output` 声明 |
| `tools/post-execute` | 同上 | — | ✔ | ✔ | 工具输出触发召回 |
| `session.append` | `dsh-session/index.d.ts` | ✘ **无时间戳/无删除** | ✘ | ✘ | 硬阻塞 |
---
## 3. 逐层对应关系
### 3.1 L3(stablemem 图记忆)→ 可映射
| HA 概念 | HA 符号 | dsh 落点 |
|---|---|---|
| `memory_blocks` 表 | `ddlMemoryBlocks`(`graph.go:2577`) | `ctx.storage.domain` 的一个 domain,或自建 `ctx.sqlite` 服务 |
| `memory_block_edges` 表 | `graph.go:320` | 同 domain 的第二张表 |
| `PutMemoryBlocks`(事务+空值不覆盖) | `block.go:203` | domain 的 `put` + 逐字段 merge 逻辑(需手写,dsh 无 `ON CONFLICT` 语义) |
| `RecallBlocks`(全表扫+余弦) | `block_recall.go:78` | 自定义 `StableMem` service 的方法 |
| `arbitrate`(时序仲裁) | `arbitration.go:375` | 纯函数,**可直接翻译**(无依赖) |
| `AbstainCheck`(拒答) | `abstain.go:98` | 纯函数,**可直接翻译** |
| `QuerySymbols`(符号提取) | `fusion.go:145` | 需 jieba(见 §4) |
| `centerBoth`(中心化) | `centroid_store.go:443` | 纯数值,可直接翻译 |
| `RecallByScene` | `scene.go:344` | SQL 前缀匹配,可翻译 |
| `stablemem_recall` 等工具 | `tooldefs.go:610-700` | `ctx.tools.register` |
| 图记忆索引注入 | `Indexer.FormatContext`(`indexer.go:358`) | `systemPrompt.context()` |
**不能映射的部分**:
- `BlocksForNode` / `NeighbourEdgesOfBlock` 的 BFS 联想(`recall-as-association.md`)
→ 需要先有边表 + 递归 CTE,SQLite 支持(`WITH RECURSIVE`),**可做但 dsh 无现成服务**。
### 3.2 L2(coldcontext 冷文档)→ 大体可映射,缺「迁回」
| HA 概念 | dsh 落点 | 可行性 |
|---|---|---|
| `ColdDoc` 存储(文件 JSON,`Store.dir`) | `ctx.storage.domain`(KV)或直接 `node:fs` | ✔ |
| `ContextToColdContext`(L1→L2 写出) | 在 `agent/turn-stopping` 里把本 turn 的消息落成 `ColdDoc` | ✔(但只能「复制」,不能「移出」L1) |
| `QueryScored` / `denseSearchScored`(召回) | 自定义 service 方法 | ✔ |
| 自动预填充(`crossModalMarkdown`) | `systemPrompt.context()` | ✔✔ |
| `coldcontext_commit` 工具 | `ctx.tools.register` | ✔ |
| `coldcontext_query` 的 `Peek→回灌→Remove` | **无对应物** | ✘ |
**为什么 `coldcontext_query` 挂不上**(最关键的一条):
HA 的实现(`toolcall.go:1121-1195`):
```go
docs := a.coldcontextStore.Peek(query, topK) // 只读,不删
plan := a.context.planColdRecall(docs, query, budget) // 超预算按页标 inactive
for i, d := range docs {
n := a.restoreDocEvents(d, plan.Evict[d.ID]) // 逐条 InsertByTimestamp
if n > 0 { a.coldcontextStore.Remove(d.ID) } // 确认落进 L1 才删 L2
}
```
其中 `restoreDocEvents`(`toolcall.go:1428`)调用
`a.context.InsertByTimestamp(ContextEvent{Timestamp: ev.Timestamp, ...})`
—— **按事件原始时间戳插进历史位置**。
dsh 侧的等价物需要:
1. `session.append('user/message', ...)` —— 只能**追加到末尾**,位置固定
2. 或者 `surfaceOp: 'replace'` —— 只能**替换一个已存在的位置**
两者都做不到「插到 turn 3 和 turn 4 之间」。
`MessageBase` 没有时间戳字段,所以即便插进去了,也无法表达「这条属于 3 小时前」。
**降级方案(§5 会展开)**:把召回结果作为**一条新的 user 消息**追加
(用 `source.form: 'recall'` —— dsh 已为这种语义预留了枚举,
`message.d.ts:58` `| 'recall';` 与 `:92` `readonly form: 'recall';`)。
语义从「迁回原时序」退化为「在末尾附一段『历史记忆(时间戳见正文)』」。
**这是有损的**:模型看不到真实的时序关系,但内容不丢。
### 3.3 L1(context 热上下文)→ 只有注入面可映射
| L1 机制 | dsh 对应 | 可行 |
|---|---|---|
| 「进 system prompt 的时序渲染」 | `systemPrompt.context()` / `agent/pre-step` | ✔ |
| 「每次输入计算向量」 | `agent/inbox/inserted` 里做 | ✔ |
| 「超限时按相关度淘汰」 | **无** | ✘ |
| 「淘汰 = 迁移到 L2(不是摘要)」 | **无** | ✘ |
| 「按页淘汰,不破 tool_call/result 配对」 | compaction 有页意识但产物是摘要 | ✘ |
| 「`InsertByTimestamp` 乱序插回」 | **无**(surface 无语义插入) | ✘ |
| 「`InactivePages` 渲染期剔除」 | **无**(surface 只能 replace 整节点) | ✘ |
**根因总结**:dsh 的上下文管理哲学是「**日志只追加,视图用摘要替换**」;
HA 的哲学是「**日志可迁移,视图是持有关系**」。
两者对「裁掉的内容去哪了」的答案不同:
- dsh:压缩成摘要(`compactSurfaceRegion`),原文在 append-only 日志里但模型看不到
- HA:迁到另一个层(`Prune` → `ContextToColdContext`),可被下次召回迁回
**这不是「接口没暴露」,是设计取向不同**。所以 L1 的 L1 层机制无法移植,
只能移植它的**效果的一半**(把记忆注入 prompt),另一半(淘汰+迁回)必须重新设计。
---
## 4. 依赖盘点:插件形态下谁提供,缺什么
| 依赖 | HA 侧 | dsh 侧现状 | 结论 |
|---|---|---|---|
| **向量化(embedding)** | `pkg/embedding` SPI(`Provider.Embed/Info/Close`)+ 4 个 provider:`chineseclip`(512 维 ONNX)、`qwen3vl`(2048 维 ONNX)、`http`(外部服务)、`StaticEmbedder`(fastText/GloVe 词向量兜底,`internal/memory/static_embedder.go`) | **完全没有**。全仓 `grep -rn "embedding" --include=*.d.ts` 只命中 UI 主题/样式相关文件,无任何 embedding/vector service | **缺。必须自建** |
| **ONNX Runtime** | `github.com/yalue/onnxruntime_go` + `-tags onnxruntime`(`providers/chineseclip/embedder.go:1`) | 无 npm `onnxruntime-node`(`find / -name onnxruntime-node` 无结果);但 `/opt/onnxruntime/libonnxruntime.so` 存在,`sherpa-onnx-linux-x64` 也自带 `libonnxruntime.so`(27MB,导出 `OrtGetApiBase`);Python 侧 `onnxruntime 1.28.0` 可用 | **缺 npm 绑定**。三条出路见下 |
| **SQLite** | `github.com/mattn/go-sqlite3`(cgo) | `node:sqlite` 内建(**实测可用**:`DatabaseSync` + FTS5 建表/匹配均通过,Node v22.23.3,仅一条 ExperimentalWarning);`dsh-session-query-sqlite` 已在用(`schema.d.ts:2` `import type { DatabaseSync } from 'node:sqlite'`) | **✔ 已具备**。可直接建 `memory_blocks` 等价表 |
| **中文分词(jieba)** | `github.com/yanyiwu/gojieba` + 词典目录(`internal/memory/jiebadict/`,`cut.go:73 GetJieba()`) | 无 `nodejieba`/`jieba-js`(`ls /usr/lib/node_modules \| grep -i jieba` 无结果) | **缺。** 三条出路:① 重写为纯 JS 词典(`QuerySymbols` 只需要词边界,不要求精确);② 用 `Intl.Segmenter`(Node 22 已内建 `zh` 词边界,**推断质量低于 jieba 但可用**);③ 只保留正则符号路(数字/Latin 串),放弃中文词 |
| **依存句法(distill)** | `internal/nlp/onnx.go`(`-tags onnxruntime`,内嵌 `dep_parser.onnx`)+ `fallback.go` 规则引擎 | 无 | **缺,但可裁剪**。HA 自己的文档(`memory-restructure-plan.md` §〇)记过:「jieba/ONNX 那条自动蒸馏路实测产出 **0 条**」。真正在用的是 LLM 拆解(`distill.Extractor.Split` 走 `pkg/generation`)+ 规则闸门 |
| **生成侧 LLM(蒸馏)** | `pkg/generation` provider(ollama/qwen3vlgen) | **✔ `ctx.llm`**(`dsh-llm` `LlmRuntime.stream(options)`);`BasicCompactionEngine.summarize()` 就是范例 | **✔ 具备**,但要走通 `registerAdapter` 或复用已有 provider |
| **媒体存储** | 块内 `Data []byte`(SQLite BLOB / JSON base64) | `ctx.attachments`(CAS,content-addressed) | **语义冲突**,见 §2.6 |
| **HTTP 外部向量服务** | `internal/memory/vector/http_embedder.go`(POST endpoint,`{modality,side,text,data,mime}` → `{embedding}`) | 无对应物,但**协议极简**(见下) | **✔ 可移植**,这是最省事的路 |
### 4.1 embedding 的三条落地路径
HA 的 `http` provider 协议(`http_embedder.go:73-86`):
```go
type httpEmbedRequest struct {
Model string `json:"model,omitempty"`
Modality string `json:"modality"`
Side string `json:"side"` // query | document
Text string `json:"text,omitempty"`
Data string `json:"data,omitempty"` // base64(非文本模态)
MIME string `json:"mime,omitempty"`
}
type httpEmbedResponse struct {
Embedding []float64 `json:"embedding"`
Data []struct{ Embedding []float64 `json:"embedding"` } `json:"data,omitempty"`
}
```
**路径 A(推荐):复用 HA 的 `http` provider 契约**。
在 dsh 插件里实现同一个 HTTP 客户端,指向一个独立进程(可以是 HA 的 `homed`
或任何 Python/Go 服务)。成本 ≈ **2 人天**。缺点:多一个常驻服务,多一个部署单元。
**路径 B:Node 内跑 ONNX**。`onnxruntime-node` 未安装,但可以 `npm i` 或
用 `ffi-napi` 绑 `/opt/onnxruntime/libonnxruntime.so`(`OrtGetApiBase` 已导出)。
成本 ≈ **6–10 人天**(C API 绑定 + 图输入预处理,Chinese-CLIP 要自己做 tokenizer + 图像预处理)。
风险:**高**。HA 的 `chineseclip` provider 有 900+ 行(`providers/chineseclip/embedder.go`),
含 tokenizer、图像 resize、归一化、session 管理。全部重写在 JS 里性价比极低。
**路径 C:纯 TF-IDF / 词袋兜底**。HA 有这条退化路径
(`ARCHITECTURE.md` 向量层级第三级)。dsh 里可用 `node:sqlite` 的 FTS5
(已实测可用)替代。成本 ≈ **2 人天**。代价:按 `memory-usability-plan.md` 的实测,
**词法检索的区分度≈0**(分数全挤在 0.75–0.79),这条路等于放弃语义召回。
### 4.2 分词的两条落地路径
`QuerySymbols`(`fusion.go:145-238`)的逻辑:
```go
// 1) 正则提取数字串 / Latin 串
for _, m := range symbolNumericRe.FindAllString(query, -1) { add(m) }
for _, m := range symbolLatinRe.FindAllString(query, -1) { add(strings.ToLower(m)) }
// 2) 中文:jieba 分词(成功则用),失败退化滑窗
cut := GetJieba()
if cut != nil {
for _, run := range cjkRuns {
for _, w := range cut.Cut(run, true) {
if w == "" || cjkFunctionWords[w] { continue }
add(w)
}
}
usedJieba = len(out) > 0
}
if !usedJieba { // 滑窗补充:2~4 字重叠窗口 + symbolStopWords 过滤
for _, run := range cjkRuns { for _, w := range cjkWindows(run) { add(w) } }
}
```
注意它**已经内置了退化路径**(`cjkWindows` 滑窗)—— 这部分是**纯字符串操作,可直接翻译**。
所以分词不是硬阻塞:**最差情况退回滑窗**,代码注释也承认「滑窗是今天的既有行为,不会更差」。
`Intl.Segmenter`(Node 22)可作为中间档:比滑窗好,比 jieba 差。
### 4.3 依赖结论
| 依赖 | 状态 | 阻塞级别 |
|---|---|---|
| SQLite | ✔ 具备(`node:sqlite`,实测通过) | 无 |
| embedding | ✘ 缺,需自建(路径 A/B/C) | **P0 阻塞**(决定 L2/L3 能否语义召回) |
| 分词 | △ 缺 jieba,有滑窗退化 | P2(可降级) |
| 依存句法 | ✘ 缺,但 HA 自己也没验证有效 | P3(直接砍) |
| LLM 蒸馏 | ✔ `ctx.llm` | 无 |
| 媒体 CAS | △ 语义冲突 | P1(裁掉媒体块) |
---
## 5. 硬阻塞项清单(按严重程度排序)+ 降级方案
### 5.1 P0 阻塞:dsh 无 embedding 服务(**必须自建**)
**证据**:全仓 `*.d.ts` 无 embedding/vector service;HA 的 `pkg/embedding` 是 200+ 行 SPI + 4 个 provider。
**影响**:L2 的 `DenseVec` 召回、L3 的 `RecallBlocks` 向量路、中心化校正、
指纹隔离**全部依赖向量**。没有向量,L2/L3 退化为 LIKE + 符号路。
**已知后果**(HA 自己的实测,`recall-capability-tiers.md`):
- 无向量 ⇒ 只有「形态 1:带精确串」可用(端口/IP/版本号)
- 「形态 2:领域词 + 上下文」从「可用」掉到「弱」
- 「形态 3:纯泛化描述」完全不可用
**降级方案**:
- **A(推荐)**:实现 §4.1 路径 A(HTTP 转发到独立进程)→ 保留全部向量能力,2 人天
- **B**:FTS5 全文检索替代向量路 → 只保留符号路 + 精确串置顶,**L3 价值降到「精确串查找+图遍历」**
- **C**:**先不移植 L3 的向量路,只移植结构(块+边+BFS)** → 见 §5.9 汇总表
### 5.2 P0 阻塞:dsh 的 surface 没有时间戳、不能语义插入(**L1 与 L2 召回的核心语义丢失**)
**证据**:
- `dsh-llm/lib/types/message.d.ts:124-136` `MessageBase` 只有 `id / content / source`
- `dsh-session/lib/types/index.d.ts:1-5`「Event-sourced session service: **append-only** session log」
- `dsh-session/lib/types/surface.d.ts:1-6`「The append-only log remains the source of truth」
- HA 侧:`context.go:998 InsertByTimestamp`、`toolcall.go:1428 restoreDocEvents`
**影响**:HA 的「召回 = 把事件按原始时间戳迁回时序」这条路径**无法表达**。
用户问「三小时前说的那个方案」,HA 把那段对话插回真实位置;dsh 只能把它写成末尾一条消息。
**降级方案**:
1. **用 `source.form: 'recall'`**(`message.d.ts:46` `export type ContextForm` 含此枚举,`:58`/`:92` 分别定义其 `form` 字面量与用途)—— 语义上
承认「这是从别处搬来的」,而非「这是新发生的事」。
2. **在正文里显式写时间戳**:「[2026-10-29 20:00 · qq] …」,让模型自己理解时序。
代价:模型可能误判因果顺序。
3. **不做「迁出」**:L2 只存「另外记下来的」内容(`coldcontext_commit` 的等价物),
不存「被淘汰的对话」。**这等于放弃 HA 的 L2 主业**(对比见 §5.8 的子集表)。
### 5.3 P1 阻塞:compaction 语义与 Prune 相反(**L1 淘汰机制不可移植**)
**证据**:`dsh-compaction-basic/lib/types/region.d.ts:40-46`
「A successful run **replaces the selected surface span with one summary node**」;
`CompactionTrigger = 'pressure' | 'context-overflow'`(`dsh-compaction/index.d.ts:25`),
**接口签名里没有 query**。
**影响**:无法做「按当前 query 相关性决定淘汰谁」。
dsh 的 compaction 是 query-agnostic 的(只看 token 压力 + retention ratio),
且产物是 LLM 摘要而非可回收的原文块。
**降级方案**:
- **A**:接受 dsh 的 compaction,记忆系统**不介入 L1 淘汰**。L2/L3 变成
「主动写入的长期记忆」而非「L1 的溢出层」。→ **这是最诚实的取舍**
- **B**:自己实现一个 `CompactionEngine` 子类,在 `summarize()` 里
额外把被替换的区间**落成 `ColdDoc`**(而不是只丢给摘要)。
这在 API 上可行(`summarize(input, agent, signal)` 拿得到 `SummarizationInput`),
但**拿不到 query**,所以淘汰依然与 query 无关。成本 +8 人天,收益有限。
- **C**:不用 compaction,用 `agent/pre-step` 自己判预算、自己 `replace` surface 节点。
**风险高**:`session.append` 的 surface 校验(`surface.d.ts` 的 `SurfaceOp`)
对「replace 必须完整覆盖被 shadow 的节点」有硬要求,自己拼容易造出必崩的帧。
### 5.4 P1 阻塞:媒体块与 dsh attachment 的 CAS 语义冲突
**证据**:`block.go:62-65`(HA 明确否决 CAS);`dsh-attachment/index.d.ts`(attachment 就是 CAS)。
**影响**:媒体块要么违反 HA 设计(用 attachment 引用),要么违反 dsh 惯例
(base64 进 KV,把 `dsh-storage-json` 的「human-readable document」撑爆)。
**降级方案**:**砍掉媒体块**。`MemoryBlock.Data` 字段保留但永不填充,
`Modality ∈ {text}` only。代价:失去跨模态召回(HA 的 `RecallBlocks` 的
`Modality` / `ExcludeText` 参数,`block_recall.go:60/63`)。
**收益**:省掉图像预处理、视觉 tokenizer、跨模态对齐的全部成本。
### 5.5 P2 阻塞:jieba 缺失
**证据**:无 npm jieba;HA 用 `github.com/yanyiwu/gojieba` + 内置词典。
**降级方案**:滑窗(HA 已有此退化路径,`fusion.go:240-268`(`cjkWindows`))或 `Intl.Segmenter`。
**影响可控**:`AbstainCheck` 的实测结论(`abstain.go:61-80`「判据的能力边界」)已经承认
「纯中文的零命中分不清『真没有』与『滑窗伪词提取失败』」——所以滑窗退化是**已知边界内**。
### 5.6 P2 阻塞:`ToolDefinition.output` 强制声明
**证据**:`dsh-tools/lib/types/index.d.ts:115` `export interface ToolDefinition extends ToolSchema` + `:117` `readonly output: ToolOutputDefinition;`(无 `?`,不是可选的)。
**影响**:每个工具多写一份 JSON Schema + render 函数。HA 侧 10+ 个 `stablemem_*` 工具
(`tooldefs.go:610-700`)每个都要补。成本 +3 人天。
### 5.7 P3 阻塞:`FrameTS` / `PageSeq` 无对应物
**证据**:dsh 用 `turn` / `step`(`TurnBoundaryProjection`,`dsh-agent/types.d.ts`),
是单调整数而非时间戳,且不参与块身份。
**降级方案**:**推断可行** —— 用 `(agent.id, turn, step)` 组合作 `FrameTS`/`PageSeq` 的等价物。
丢失的是「跨 session 的时间可比性」,但 HA 的 `FrameTS` 主要用途(同帧块不跨层、
页内配对)在单 session 内可满足。
### 5.8 降级方案:只移植某几层成不成立?
| 子集 | 成立? | 理由 | 工作量 |
|---|---|---|---|
| **只有 L3**(图记忆) | ✔✔ **最成立** | L3 自包含:块+边+5 条召回链路全是纯算法 + SQLite。不依赖 L1/L2 的任何机制。作为「事实与关系库」独立存在完全合理。 | **19–25 人天**(= §6.2 的 MVP 口径:阶段 0+1+2+3+7) |
| **只有 L2**(冷文档) | △ 半成立 | 存储/召回/预填充都能做,但**放弃「迁出-迁回」**后,L2 只能靠显式写入(等价 `coldcontext_commit`)填充,不能自动从 L1 溢出。价值从「对话历史归档」降为「手工笔记库」。 | 12–17 人天 |
| **只有 L1**(热上下文) | ✘ **不成立** | L1 的全部价值在「淘汰+迁回」这对机制上。只剩下「往 prompt 注入东西」,那不需要三层记忆,一个 `PromptContext` 就够。 | — |
| **L2 + L3** | ✔✔ **推荐** | 二者都以「块」为单元、都以「召回进 prompt」为出口,共享 embedding + SQLite。L1 完全交给 dsh 自己的 compaction。 | **36–53 人天**(= §6.2 合计) |
| **L1 + L2** | △ | 缺 L3 的关系能力,但至少「归档+召回」闭环成立。**前提是接受 §5.2 的时间戳降级**。 | 24–34 人天 |
| **三层全搬** | ✘ | L1 的两条核心机制(按 query 淘汰、时间戳迁回)在 dsh 里无对应扩展点,必须改 dsh 内核或另造一套 surface 管理。**这不是插件能做的事**。 | — |
### 5.9 硬阻塞汇总表
| # | 阻塞项 | 严重度 | 可绕过? | 绕过代价 |
|---|---|---|---|---|
| 1 | dsh 无 embedding 服务 | **P0** | ✔ 自建(HTTP 转发最省) | 2 人天 + 一个常驻服务 |
| 2 | 消息无时间戳、surface 不能语义插入 | **P0** | △ 只能降级(`form:'recall'` + 正文写时间) | **语义损失,不可完全恢复** |
| 3 | compaction = 摘要替换 ≠ Prune 迁移 | **P1** | △ 接受它 / 再写一个后端 | L1 淘汰机制放弃 |
| 4 | attachment CAS ≠ 块内联字节 | **P1** | ✔ 砍掉媒体块 | 失去跨模态召回 |
| 5 | 无 jieba | P2 | ✔ 滑窗 / `Intl.Segmenter` | 中文符号提取变弱(已知边界内) |
| 6 | `ToolDefinition.output` 强制 | P2 | ✘ 必须写 | +3 人天 |
| 7 | `FrameTS` 无对应物 | P3 | ✔ `(session,turn,step)` 组合 | 跨 session 时间可比性丢失 |
| 8 | `ON CONFLICT ... 空值不覆盖` 语义 | P3 | ✘ 手写 merge 逻辑 | +2 人天(易出错,需测试) |
---
## 6. 结论与工作量
### 6.1 结论:**只能做子集**
**理由链**(逐条对应证据):
1. **L3 可以几乎完整移植**,因为它的依赖(SQLite + 纯算法 + 工具注册)在 dsh 全部具备。
`node:sqlite` 实测可用(`DatabaseSync` + FTS5),`ToolRuntime.register` 是完备的工具面,
`arbitrate` / `AbstainCheck` / `QuerySymbols` / `centerBoth` 都是纯函数,可 1:1 翻译。
2. **L2 可以移植 80%**,缺的是「回收站语义」(`Peek → 回灌 → Remove`)。
dsh 侧可以用 `ctx.storage.domain`(或直接 `node:sqlite`)建出等价的 `ColdDoc` 存储,
可以存、可以查、可以注入,但**不能把内容从 L1 移出**,
所以「L2 作为 L1 的溢出层」这个定位无法成立。降级为「主动写入的长期记忆」仍可用。
3. **L1 无法移植**。这是决定性的一条:L1 的两个不可替代机制
——(a)按当前 query 相关性淘汰、(b)按原始时间戳把事件插回历史位置 ——
在 dsh 的扩展点上**都没有落点**。`ctx.compaction` 的接口签名里没有 query
(`CompactionTrigger` 只有两个枚举值),`MessageBase` 没有时间戳字段。
要支持这两点必须改 dsh 内核(新增 surface 操作或扩展 compaction 接口),
那已经不是「插件」的范畴。
4. **因此「三层移植」这个命题本身要修正**:能移植的是
「**块化的长期记忆(L2+L3)**」,L1 应替换为 dsh 自己的 compaction。
### 6.2 工作量粗估
| 阶段 | 内容 | 人天 |
|---|---|---|
| 0 | 骨架:插件包、`Service` 注册、配置、`node:sqlite` 建表(`memory_blocks` / `memory_block_edges` / `graph_centroid`) | 3 |
| 1 | **embedding 桥**(HTTP 转发 + fingerprint + 维度/指纹跳过逻辑) | 2–3 |
| 2 | **块与边**:`MemoryBlock` 值对象、确定性 ID 派生、`PutMemoryBlocks` 的「空值不覆盖」merge、`AddMemoryBlockEdge` 端点校验 | 5–7 |
| 3 | **L3 召回**:`RecallBlocks` + 中心化 + 指纹隔离 + 模态过滤 | 4–5 |
| 4 | **L3 增强**:`QuerySymbols`(含滑窗退化)+ 融合 + `arbitrate` + `AbstainCheck` | 5–7 |
| 5 | **L2 存储**:`ColdDoc`/`ColdEvent` + `docBlocksForEntry`/`InOut` 对称拆装 + 向量召回 | 4–6 |
| 6 | **注入面**:`agent/pre-step` 的自动预填充(等价 `crossModalMarkdown`)+ `systemPrompt.context()` 的常驻记忆统计 | 2–3 |
| 7 | **工具面**:`stablemem_recall/commit/merge/purge` + `coldcontext_query/commit`(每个都要 `output` 声明) | 5–7 |
| 8 | **落盘与恢复**:`agent/turn-stopping` 触发归档、`agent/disposed` 冲刷、`session/flush` | 2–3 |
| 9 | 判据与实测 | 4–6 |
| | **合计(L2+L3 子集)** | **36–53** |
**压缩到最小可用(MVP)**:阶段 0+1+2+3+7(去掉 L2 与仲裁/拒答)≈ **19–25 人天**。
**若强行包含 L1 的降级版**(`form:'recall'` + 正文写时间戳,不做淘汰):
+8–12 人天,**但价值增量很低**(因为淘汰是 L1 的核心,注入面本来就有)。
> 工作量口径统一说明:**请用上表**(含判据与实测)。§5.8 的子集表已按此口径对齐。
---
## 7. 建议落地阶段 + 可验证的最小第一步切片
### 7.1 阶段划分
**阶段 A:第一步切片(1–2 人天,必须先做)**
目标:**证明「块能进 dsh、能被召回、能进 prompt」这条最小链路成立**,
且**不引入任何外部服务依赖**。
具体切片:
```ts
// A1. 新建插件包 dsh-memory,导出 apply(ctx, config)
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'memory'
export const inject = ['systemPrompt', 'tools'] // 与 dsh-working-directory 同构
export function apply(ctx: Context, config: Config) {
const mem = new StableMem(ctx) // super(ctx, 'stablemem')
// A2a. 注入面:静态/动态 context 段(已核对 dsh-working-directory/lib/index.js:41)
ctx.systemPrompt.context({
name: 'memory:index',
order: ctx.systemPrompt.getContextOrder('WEB_SURFACE'), // 或直接给 210
interpolate: false,
// ⚠ text() 只拿得到 AssembleContext{scope, signal, agent?},拿不到“本次输入文本”
text: (c) => (c.agent === undefined ? '' : mem.renderIndex(c.agent.session)),
})
// A2b. ★ 真正能拿到“当前这一步要进模型的消息”的钩子是 agent/pre-step(waterfall)。
// 已核对 dsh-time-context/lib/index.js:218 的用法:
// const decision = await next() → 改 decision.messages → return
ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next) => {
const decision = await next()
if (decision.kind === 'reject' || signal.aborted) return decision
const injected = mem.recallForStep(agent, decision.messages) // 用本步输入做召回
if (injected === '') return decision
return {
...decision,
messages: [...decision.messages, createUserMessage({
content: [{ type: 'text', text: injected }],
source: { kind: name, form: 'snapshot', sections: [{ name, text: injected }] },
})],
}
}, { prepend: true })
// A2c. 工具面(output.schema + output.render 是强制的)
ctx.tools.register(defineTool({
name: 'stablemem_commit',
description: '...',
parameters: { /* ... */ },
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
async execute(args, exec) { return mem.commit(args) },
}))
}
// A3. StableMem 用 node:sqlite 建 memory_blocks(**只建 text 模态**)
// import { DatabaseSync } from 'node:sqlite' ← 已实测可用(含 FTS5)
// A4. 召回先**不走向量**:FTS5 + 精确串置顶(把 QuerySymbols 的
// 数字/Latin 正则部分 + 滑窗部分翻成 TS)
// A5. 判据(必须能红):
// ✔ 工具写入一个块 → 下个 pre-step 的 messages 里出现它
// ✔ 变异:recallForStep 返回 '' ⇒ 判据红
// ✔ 变异:把 FTS5 MATCH 改成 LIKE '%x%' 全命中 ⇒ 判据红(区分度)
```
> **★ A2a / A2b 的分工必须说清**(这是阶段 A 最重要的发现):
> `systemPrompt.context()` 适合**与本次输入无关**的常驻内容(工作目录、沙箱策略、
> 记忆总量统计);需要**按本次输入做召回再注入**时,必须走 `agent/pre-step`,
> 因为 `PromptContext.text(context: AssembleContext)` 的 `AssembleContext`
> 只有 `{scope?, signal?}` + 合并进来的 `agent?`,**没有本步消息正文**。`dsh-time-context` 正是因此选择在 `agent/pre-step`
> 里 `createUserMessage` 后追加到 `decision.messages`(`lib/index.js:218-243`)。
**为什么这个切片是对的**:
- 它**故意不碰 embedding**(P0 阻塞),用 FTS5 拿掉第一个硬依赖
- 它**故意不碰 L1**(不可移植),只验证注入面
- 它**故意不碰 L2 的召回**(需要时间戳降级方案定型)
- 它验证的正是「最有价值且最可移植」的部分:**块 + 存储 + 召回 + 注入**
- 失败成本极低:如果 `systemPrompt.context()` 的求值时机或
`node:sqlite` 的并发行为不符预期,**在 2 天内就能发现**
**验证方式**(可直接跑的 checkpoints):
| # | 检查 | 命令/方法 |
|---|---|---|
| V1 | `node:sqlite` 在插件进程内可用、WAL 模式正常 | 建库 + 并发写 100 次 + 读回 |
| V2 | `node:sqlite` FTS5 对中文的召回质量 | 用 HA 的 50 条真实块文本做语料,10 个查询测 top1 |
| V3 | `systemPrompt.context()` 的 `text(ctx)` 能拿到 `agent.session`,但**验证它拿不到本步消息** | 在 text 里 log `Object.keys(context)`,确认只有 `scope/signal/agent` |
| V3b | `agent/pre-step` 的 `decision.messages` 可被追加且模型的下一轮确实看到 | 最小 echo 注入 + 观察 `/dump-config` 或 session log 里是否新增 `user/message` |
| V4 | `ctx.tools.register` 的 `output.schema`/`render` 契约 | 最小 echo 工具跑通 |
| V5 | 插件卸载后 sqlite 句柄释放 | `ctx.dispose` + 文件锁检查 |
**阶段 A 的 exit criteria**:V1–V5 全绿 + 判据 A5 三条(含 2 条变异)
+ **V3/V3b 的分工结论写入插件 README**(哪个钩子能拿输入,哪个不能)。
**阶段 B:向量落地(3–4 人天)**
接 §4.1 路径 A(HTTP 转发)或路径 C(确认放弃向量)。
在此阶段把 `RecallBlocks` 的余弦 + 指纹/维度跳过 + 中心化 补齐。
**阶段 C:L3 完整召回(8–12 人天)**
`QuerySymbols` 完整版 + 融合 + 仲裁 + 拒答 + 场景召回。
**阶段 D:L2 冷文档(6–9 人天)**
`ColdDoc`/`ColdEvent` + 对称拆装 + 向量召回 + `systemPrompt.context()` 的预填充。
**前置条件**:§5.2 的时间戳降级方案已拍板(`form:'recall'` + 正文写时间)。
**阶段 E:工具面 + 归档触发(7–10 人天)**
全部 `stablemem_*` / `coldcontext_*` 工具;`agent/turn-stopping` 触发归档。
**阶段 F:判据与实测(4–6 人天)**
按 HA 的判据风格(「变异必须能红」)建测试;
用 HA 的生产库快照做 A/B 对比(`recall-capability-tiers.md` 的四形态 + 排名基线)。
### 7.2 必须写进设计文档的三条「不可移植」声明
避免后续被当成 bug 修:
1. **「召回不会把事件插回原时序」** —— dsh surface 不支持;只能 `form:'recall'` 追加。
2. **「L1 淘汰由 dsh compaction 负责,不由记忆插件负责」** —— 语义是摘要不是迁移。
3. **「媒体块不支持」** —— dsh attachment 是 CAS,与 HA「块内联字节」的设计冲突。
这三条都应在插件 README 的第一屏写明,并在 `systemPrompt.context()` 的注入文案里
向模型说明(否则模型会按 HA 的语义预期行为)。
---
## 附录 A:证据索引
### A.1 HologramAgent 侧(只读,未修改)
| 主题 | 文件:符号 |
|---|---|
| `MemoryBlock` 定义 | `internal/memory/block.go:66` |
| 块 union 负载 / 无引用账本(否决 CAS) | `internal/memory/block.go:55-56`、`62-65` |
| 段页式身份(FrameTS/PageSeq/CallID) | `internal/memory/block.go:100-159` |
| `PutMemoryBlocks`(空值不覆盖) | `internal/memory/block.go:203-300` |
| `AddMemoryBlockEdge` | `internal/memory/block.go:497` |
| `memory_blocks` DDL | `internal/memory/graph.go:2577` |
| `memory_block_edges` DDL | `internal/memory/graph.go:320` |
| `RecallBlocks` | `internal/memory/block_recall.go:78` |
| `RecallBlocksWithArbitration` | `internal/memory/block_recall.go:229` |
| `largeTopK = 2000` | `internal/memory/block_recall.go:263` |
| `Centroid` / `BuildCentroid` | `internal/memory/centroid.go:61/199` |
| `centerBoth` | `internal/memory/centroid_store.go:443` |
| `AbstainCheck` | `internal/memory/abstain.go:98` |
| `RecallBlocksGuarded` | `internal/memory/abstain.go:179` |
| `arbitrate` | `internal/memory/arbitration.go:375` |
| `parseFact` / `supersedes` | `internal/memory/arbitration.go:88/148` |
| `QuerySymbols` | `internal/memory/fusion.go:145` |
| 精确串布尔置顶 | `internal/memory/fusion.go:427` |
| `RecallByScene` | `internal/memory/scene.go:344` |
| `ContextEvent` | `internal/agent/core/context.go:64` |
| `RelevanceContext` | `internal/agent/core/context.go:145` |
| `Append` | `internal/agent/core/context.go:967` |
| `InsertByTimestamp` | `internal/agent/core/context.go:998` |
| `PruneWithProtected` | `internal/agent/core/context.go:1075` |
| `planColdRecall` / `ColdRecallPlan` | `internal/agent/core/context.go:453/489` |
| `ColdDoc` / `ColdEvent` | `internal/memory/coldcontext/coldcontext.go:37/75` |
| `ContextToColdContext` | `internal/memory/coldcontext/coldcontext.go:385` |
| `Peek` / `Consume` | `internal/memory/coldcontext/coldcontext.go:480/526` |
| `docBlocksForEntry` | `internal/memory/coldcontext/coldcontext.go:974` |
| `ColdEvent.InOut` | `internal/memory/coldcontext/coldcontext.go:1259` |
| `executeDocTool` | `internal/agent/core/toolcall.go:1090` |
| `restoreDocEvents` | `internal/agent/core/toolcall.go:1428` |
| `recallByBlocks` | `internal/agent/core/toolcall.go:1257` |
| `crossModalMarkdown` | `internal/agent/core/crossmodal.go:256` |
| L2 预填充调用点 | `internal/agent/core/tooldefs.go:51` |
| `buildSystemPrompt` | `internal/agent/core/tooldefs.go:176` |
| `BlockID` 派生 | `internal/memory/distill/blocks.go:99` |
| `FieldBlock.ToMemoryBlock` | `internal/memory/distill/blocks.go:126` |
| `Extractor.Blocks` | `internal/memory/distill/blocks.go:69` |
| `embedding.Provider` SPI | `pkg/embedding/embedding.go`(`Input`/`Info`/`Provider`/`Register`) |
| `http` embedding provider 协议 | `internal/memory/vector/http_embedder.go:72-90` |
| `chineseclip` provider(ONNX) | `providers/chineseclip/embedder.go:1`(`//go:build onnxruntime`) |
| `StaticEmbedder`(词向量兜底) | `internal/memory/static_embedder.go:32` |
| jieba 获取 | `internal/memory/cut.go:73 GetJieba()` |
| 依赖清单 | `go.mod`(`go-sqlite3` / `gojieba` / `onnxruntime_go`) |
| 设计文档 | `docs/zh/memory-restructure-plan.md`, `memory-blockflow-repair-plan.md`, `memory-usability-plan.md`, `recall-as-association.md`, `recall-capability-tiers.md`, `multimodal-space.md`, `context-overflow-l4-design.md`, `kernel-modularization-design.md`, `input-scheduler-design.md` |
| 权威架构 | `assets/docs/zh/ARCHITECTURE.md:180-330` |
### A.2 dsh 侧
| 主题 | 文件 |
|---|---|
| cordis `Context` / `isolate` / `intercept` | `@deepseek-ai/cordis/lib/types/context.d.ts` |
| `Service` 注册基类 | `@deepseek-ai/cordis/lib/types/service.d.ts:38` |
| 事件总线模式(emit/parallel/serial/bail/waterfall) | `@deepseek-ai/cordis/lib/types/events.d.ts` |
| `AgentRegistry` | `@deepseek-ai/dsh-agent/lib/types/index.d.ts` |
| Agent 生命周期事件全部枚举 | `@deepseek-ai/dsh-agent/lib/types/runtime-types.d.ts` |
| `agent/pre-step` → `PreStepDecision` | 同上(`'agent/pre-step'` waterfall) |
| `Agent` 的 `send/followup/steer/inject/runMaintenance/cancel` | 同上(`declare module './types.ts' { interface Agent { ... } }`) |
| agent-loop 工厂 / `TurnBoundaryProjection` | `@deepseek-ai/dsh-agent-loop/lib/types/index.d.ts`、`agent.d.ts` |
| `SystemPrompt.section/context/tools/variable/assemble` | `@deepseek-ai/dsh-system-prompt/lib/types/index.d.ts:236-320`(`context()` 在 :269) |
| `SECTION_ORDERS` / `CONTEXT_ORDERS` | 同上 :119-163 |
| `system-prompt/assemble` waterfall | 同上 :27 |
| `ToolRuntime.register/restrict/schemas` | `@deepseek-ai/dsh-tools/lib/types/index.d.ts:637/654/712` |
| `ToolDefinition`(`output` 强制,:117) | 同上 :115-200 |
| `tools/pre-execute` / `execute` / `post-execute` / `result` / `change` | 同上 :48-105 |
| `Session` / `append` / `deriveMessages` | `@deepseek-ai/dsh-session/lib/types/index.d.ts:135/347` |
| `SessionSurface`(append-only) | `@deepseek-ai/dsh-session/lib/types/surface.d.ts:1-30` |
| `MessageBase`(**无时间戳**) | `@deepseek-ai/dsh-llm/lib/types/message.d.ts:124-136` |
| `ContextForm` / `ContextFormed`(含 `'recall'`) | `@deepseek-ai/dsh-llm/lib/types/message.d.ts:46-93` |
| `MessageSourceMap` | 同上 :98-105 |
| `CompactionEngine` | `@deepseek-ai/dsh-compaction/lib/types/index.d.ts` |
| `compactSurfaceRegion`(摘要替换) | `@deepseek-ai/dsh-compaction-basic/lib/types/region.d.ts:40-60` |
| `CompactionTrigger` | `@deepseek-ai/dsh-compaction/lib/types/index.d.ts:25` |
| `BasicCompactionEngine.summarize`(唯一钩子) | `@deepseek-ai/dsh-compaction-basic/lib/types/index.d.ts` |
| `ToolResultPruner.pruneSession` | `@deepseek-ai/dsh-compaction-tool-result-pruner/lib/types/index.d.ts` |
| `AttachmentStore`(CAS) | `@deepseek-ai/dsh-attachment/lib/types/index.d.ts` |
| `Storage` hub / `StorageBackend` / `KvFacet` | `@deepseek-ai/dsh-storage/lib/types/index.d.ts`、`backend.d.ts` |
| `DomainFacility` / `defineDomain` | `@deepseek-ai/dsh-storage-domain/lib/types/index.d.ts` |
| JSON backend(整 unit 一文档) | `@deepseek-ai/dsh-storage-json/lib/types/index.d.ts` |
| `node:sqlite` 先例 | `@deepseek-ai/dsh-session-query-sqlite/lib/types/schema.d.ts:2` |
| `SessionQueryEngine`(FTS5 全文检索) | `@deepseek-ai/dsh-session-query/lib/types/index.d.ts` |
| `TokenMeter` | `@deepseek-ai/dsh-token-meter/lib/types/index.d.ts` |
| `LlmRuntime` / `LlmAdapter` / `stream` | `@deepseek-ai/dsh-llm/lib/types/index.d.ts:130/245` |
| `Scoped` / `ScopeKey` | `@deepseek-ai/dsh-scope/lib/types/index.d.ts` |
| Loader / Include / Entry | `@deepseek-ai/cordis-plugin-loader/lib/types/index.d.ts`、`cordis-plugin-include/lib/types/index.d.ts` |
| `dsh-base`(无运行期 API,纯 patch bundle) | `@deepseek-ai/dsh-base/lib/types/index.d.ts` |
### A.3 环境实测
| 项 | 结果 | 方法 |
|---|---|---|
| `node:sqlite` 可用 + FTS5 | ✔ 通过(`CREATE VIRTUAL TABLE ... USING fts5` + `MATCH`) | `node -e` 直跑,Node v22.23.3 |
| `onnxruntime-node` npm 包 | ✘ 未安装 | `find / -name onnxruntime-node -type d` 无结果 |
| `libonnxruntime.so` | ✔ 存在两处:`/opt/onnxruntime/`(24MB)、`/usr/lib/node_modules/sherpa-onnx-linux-x64/`(27MB,导出 `OrtGetApiBase`) | `ls` + `nm -D` |
| jieba (JS) | ✘ 未安装 | `ls /usr/lib/node_modules \| grep -i jieba` 无结果 |
| Python `onnxruntime` | ✔ 1.28.0 | `python3 -c "import onnxruntime"` |
| dsh 版本 | `0.2.1-rc.2` | `@deepseek-ai/dsh/package.json` |
| cordis 版本 | `4.0.5` | `@deepseek-ai/cordis/package.json` |
---
## 附录 B:本文的取证纪律
- **所有 dsh 接口结论均来自 `lib/types/*.d.ts`**(权威接口),未从 `lib/*.js` 实现倒推。
- **所有 HA 结论均来自源码或 `docs/zh/*.md`**,并标注了符号名(行号仅作辅助锚点,
文档本身也说明「行号随改动漂移,函数名才是稳定锚点」)。
- 标「**推断**」的地方只有三处:`turn`≈`FrameTS` 的映射、`Intl.Segmenter` 的中文分词质量、
§6.2 的人天估算。其余均为可直接复核的事实。
- HA 仓库(`/home/program/TrueAgent/`)**全程只读**,未做任何修改(`git status` 无输出)。