灯下哥谭 灯下哥谭
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

灯下哥谭

灯还亮着
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • OpenClaw

  • Hermes-Agent

  • Claude-Code

  • LLM推理

  • DeepSeek-Harness

    • DeepSeek Harness 实战 01|同一条纪律,写在两个位置,模型只听一个
    • DeepSeek Harness 实战 02|三个 Harness 的解剖:同一个文件名,三种加载语义
    • DeepSeek Harness 实战 03|学习笔记:读 12 篇 README 拿下 dsh 的五个核心心智模型
    • DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力
    • DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆
      • 1. 会话结束即失忆,换个工具再失忆一次
      • 2. 读和写是两条完全不同的通路
      • 3. 读侧:把记忆库挂成 dsh 的 MCP client
        • 准备:探测路径,不要写死
        • 执行:先让 server 自己说话
        • 执行:写进 dsh 的 patch 层
        • 验证:必须做阳性对照
      • 4. 写侧:那个看起来多余的硬链接跳板
        • 症状:全对,且零入库
        • 根因:Bun 的递归 fs.watch 不认后建目录
        • 解药:把「已完稿」的会话发布到一个早就存在的扁平目录
        • 然后把 watcher 指向扁平目录
      • 5. 会话格式:字段路径怎么核对
      • 6. 八个不报错的失效点
      • 7. 把每条坑变成一个会失败的断言
      • 8. 隐私:默认采集了什么
      • 9. 可复用要点
      • 10. 延伸阅读
  • AI-Agent
  • DeepSeek-Harness
灯下哥谭
2026-09-09
目录

DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆原创

# 让两个编码 Agent 共用一份长期记忆:一次全程无报错的静默失效

把一个持久记忆库接到 DeepSeek Harness(下称 dsh)上,最直觉的做法是:记忆库自带转录监听器,那就把它指向 dsh 的会话目录,让它自己去读。配置字段全对,监听器启动无异常,日志一行报错没有,会话文件明明白白躺在那个目录里——然后你等一小时,数据库里一条记忆都没有。

问题不在配置,在于监听器跑在 Bun 上,而 Bun 的递归 fs.watch 不会给 watch 启动之后才新建的目录挂 inotify 监听;dsh 恰好每个会话新建一个目录。这不是多等一会儿能赢的竞态,是结构性看不见。这篇把这个集成从头做通,连同另外七个同样不报错的失效点,最后给出一套把每条坑变成可失败断言的验证脚本。全部代码开源在 Carry00/dsh-claude-mem (opens new window)。

版本说明

本文基于 dsh developer preview 与 claude-mem v13.x 实测。dsh 的会话记录格式(session.v3.jsonl)仍在演进,字段路径以你本机的实际转录为准——文中给了逐字段核对的方法。


# 1. 会话结束即失忆,换个工具再失忆一次

编码 Agent 的记忆有两层各自独立的丢失。

第一层是会话级的。 上次踩过的坑、定过的方案、排查出来的根因,新开一个会话一概不知道,你得重讲一遍。这不是上下文窗口不够——窗口再大,会话一结束照样散掉。它需要的是一个会话之外的存储。

第二层是工具级的。 假设你已经解决了第一层:某个记忆库(本文用 claude-mem,一个把会话摘要成 observation 并提供检索的本地 SQLite 方案)替你记住了几个月的工作。但那是另一个 harness 的库。你切到 dsh,等于换了一个大脑:两边各记各的,谁也读不到谁。

这两层合起来的效果是:你在 A 工具里花两小时排查出的根因,在 B 工具里不存在。工具越多越严重。

所以目标不是「给 dsh 装个记忆」,而是让 dsh 和已有工具共用同一个记忆池——问 dsh「上次这个报错的根因是什么」,它能捞到你在另一个 harness 里的排查过程,反过来也一样。


# 2. 读和写是两条完全不同的通路

一个共享记忆是两个方向,机制完全不同,可以只做一半:

方向 做什么 机制
读 dsh → 记忆 agent 能检索历史上记录过的一切 把记忆库的 MCP server 注册成 dsh 的 MCP client
写 dsh → 记忆 dsh 自己的会话被摘要入库,日后可检索 给记忆库的转录监听器喂一份 dsh 会话格式的 schema

整体形状:

                    ┌──────────────────────────────┐
   读   ────────────│  记忆库 MCP server           │◀── 另一个 harness 写入的同一个库
                    │  (mcp-server.cjs, stdio)     │
                    └──────────────┬───────────────┘
                                   │ mcp__<name>__search …
                    ┌──────────────▼───────────────┐
                    │            dsh               │
                    └──────────────┬───────────────┘
                                   │ 写 session.v3.jsonl
   ${DSH_HOME}/sessions/<workspace>/<session-id>/session.v3.jsonl
                                   │
                                   │  静置后硬链接(timer,每 2 分钟)
                                   ▼
   ${DSH_HOME}/sessions-cmem/<session-id>.jsonl   ← 扁平、稳定、早已存在的目录
                                   │
                    ┌──────────────▼───────────────┐
   写   ────────────│  记忆库 transcript watcher   │──▶ observations
                    └──────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

读侧是纯配置,半小时能通。写侧那个多出来的硬链接跳板,就是本文的主要内容——它看起来像是脱裤子放屁,实际是绕开一个没法从配置层解决的运行时限制。


# 3. 读侧:把记忆库挂成 dsh 的 MCP client

先做读侧,因为它有一个极其好用的早期验证点:MCP server 能不能握手,一条命令就能问出来,不需要动 dsh。

# 准备:探测路径,不要写死

记忆库的安装路径因安装方式而异,而且同一台机器上常常并存多个版本(一个已安装的 marketplace 副本,加若干版本锁定的 cache 副本)。探测,别假设:

CM_HOME="${CLAUDE_MEM_HOME:-$HOME/.claude-mem}"      # 数据库与配置
CC_CONFIG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"      # 宿主配置目录

# 优先取 marketplace 安装副本,而不是 cache 里的旧版本
cands=$(find "$CC_CONFIG" -name mcp-server.cjs 2>/dev/null)
pick=$(printf '%s\n' "$cands" | grep marketplaces | head -1)
[ -z "$pick" ] && pick=$(printf '%s\n' "$cands" | head -1)
CM_PLUGIN=$(dirname "$(dirname "$pick")")
echo "$CM_PLUGIN"
1
2
3
4
5
6
7
8
9

这一步不做,最常见的后果是你接上了一个几个版本之前的 cache 副本,工具列表和你读的文档对不上。

# 执行:先让 server 自己说话

任何 stdio MCP server 都可以用三行 JSON-RPC 直接问,不需要任何客户端:

printf '%s\n' \
 '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
 '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
 '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| CLAUDE_CONFIG_DIR="$CC_CONFIG" CLAUDE_PLUGIN_ROOT="$CM_PLUGIN" \
  timeout 30 node "$CM_PLUGIN/scripts/mcp-server.cjs" 2>/dev/null | head -2
1
2
3
4
5
6

预期输出(第一行是握手,第二行是工具清单):

{"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"claude-mem","version":"13.24.1"}},"jsonrpc":"2.0","id":1}
{"result":{"tools":[{"name":"important_workflow",...},{"name":"search",...
1
2

这条命令还顺带告诉你它到底是哪个版本——上一步选错副本的话,这里的 version 会当场露馅。

# 执行:写进 dsh 的 patch 层

dsh 的家目录级 patch(${DSH_HOME:-$HOME/.dsh}/cordis.patch.yml)对所有 profile 生效,优先级高于 profile 层。插入一个 MCP client 条目:

- insert:
    - id: claude-mem-mcp
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: claude_mem
        transport: stdio
        command: node
        args:
          - <CM_PLUGIN>/scripts/mcp-server.cjs
        env:
          CLAUDE_CONFIG_DIR: <CC_CONFIG>
          CLAUDE_PLUGIN_ROOT: <CM_PLUGIN>
1
2
3
4
5
6
7
8
9
10
11
12

两个字段是承重的,写错了工具根本不会出现,且没有任何提示:

  1. 直接指向 mcp-server.cjs,不要指向包自带的启动器。 那类启动器普遍会做路径探测,而它探的那些路径在 dsh 的进程启动环境下解析不出来。
  2. env 必须显式声明。 dsh 的 stdio 桥接会把环境变量名匹配 /KEY|PASSWORD|SECRET|TOKEN/i 的、以及所有 DSH_* 的,全部洗掉再传给子进程。这是个好的默认(防止把凭据泄漏给第三方 MCP server),代价是依赖环境变量的 server 必须在这里把需要的名字一个个列出来。

# 验证:必须做阳性对照

开一个新 dsh 会话,让它检索一个你确知库里有的词,并要求它报出它实际调用的工具名和第一条结果的标题。

不要接受「没搜到」作为通过。 这是本文最贵的一条经验:agent 会把一条坏掉的管道如实包装成「未找到相关结果」,还附上一段自信的总结。用一个必然命中的词(任意一个你做过的项目名)做阳性对照,看到真实标题才算通。


# 4. 写侧:那个看起来多余的硬链接跳板

现在是主菜。

# 症状:全对,且零入库

记忆库的转录监听器配置长这样(简化):

{
  "watches": [
    { "name": "dsh", "schema": "dsh",
      "path": "/home/<user>/.dsh/sessions/**/session.v3.jsonl",
      "startAtEnd": false }
  ]
}
1
2
3
4
5
6
7

路径对、通配符对、schema 也对。重启监听器进程,跑几个 dsh 会话,数据库里什么都没有。日志里没有 error,没有 warning,watcher 状态文件里连一个字节偏移量都没记。

# 根因:Bun 的递归 fs.watch 不认后建目录

监听器进程跑在 Bun 上。Bun 的 fs.watch({recursive:true}) 在启动时对当时已存在的目录树逐个挂 inotify watch;之后新建的子目录,不会被自动补挂。

而 dsh 的会话布局是一个会话一个新目录:

${DSH_HOME}/sessions/<workspace-slug>/<session-id>/session.v3.jsonl
1

于是每一个新会话都诞生在一个 watcher 从未挂过监听的目录里。文件被创建、被追加、被写完,inotify 全程没有事件产生。这不是时序问题——再等多久都不会到。

这个失效模式的恶劣之处在于三重静默:Bun 不报错(它确实按规范挂了当时存在的目录)、监听器不报错(它没收到事件,也就无从知道漏了什么)、dsh 不报错(它压根不知道有人在读它的日志)。

# 解药:把「已完稿」的会话发布到一个早就存在的扁平目录

既然「后建的目录看不见」,那就不要让 watcher 去看后建的目录。改成:让一个定时任务,把已经静置(连续 N 秒没被写过,即会话已停笔)的转录,发布到一个 watch 启动时就已经存在的扁平目录里。那个目录深度为 1 的 create 事件,投递是可靠的。

#!/bin/bash
set -euo pipefail

SRC="$HOME/.dsh/sessions"
DST="$HOME/.dsh/sessions-cmem"
SETTLE_SECS="${DSH_CMEM_SETTLE_SECS:-120}"

mkdir -p "$DST"
[ -d "$SRC" ] || exit 0

find "$SRC" -name 'session.v3.jsonl' -type f -mmin +$((SETTLE_SECS/60)) | while read -r f; do
  sid=$(basename "$(dirname "$f")")
  target="$DST/$sid.jsonl"
  [ -e "$target" ] && continue
  ln -f "$f" "$target" 2>/dev/null || cp -f "$f" "$target"
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

三个设计点值得说清楚:

  • 用硬链接,不用拷贝。 同一文件系统,不占额外磁盘;更重要的是,会话若被恢复并继续追加,硬链接看得到追加内容,watcher 的字节偏移量顺势往前走即可,不需要重新发布。跨文件系统时 ln 会失败并回退到 cp——功能上没问题,但从此刻起追加就跟不上了,这是个要知道的降级。
  • 只发布静置的会话。 正在写的会话不发布,避免 watcher 读到半截。代价是延迟。
  • 幂等。 目标已存在就跳过,脚本可以任意频率重复跑。

挂到一个两分钟的 systemd user timer 上:

# ~/.config/systemd/user/dsh-cmem-export.timer
[Timer]
OnBootSec=3min
OnUnitActiveSec=2min
AccuracySec=30s

[Install]
WantedBy=timers.target
1
2
3
4
5
6
7
8
systemctl --user daemon-reload
systemctl --user enable --now dsh-cmem-export.timer
1
2

没有 systemd 的话 */2 * * * * $HOME/.dsh/cmem-export.sh 等效。

这套设计的代价要说明白:会话停笔到记忆可检索之间有 2–4 分钟延迟(最多 120 秒静置 + 最多 120 秒 timer 间隔)。这是为可靠性主动付的成本,不是缺陷。要更快就调小 SETTLE_SECS 和 timer 间隔,但静置窗口小于会话的思考停顿时,就会开始发布半截会话。

# 然后把 watcher 指向扁平目录

{
  "watches": [
    { "name": "dsh", "schema": "dsh",
      "path": "/home/<user>/.dsh/sessions-cmem/*.jsonl",
      "startAtEnd": false }
  ]
}
1
2
3
4
5
6
7

两个设置必须正确,各自对应一个后面会讲的坑:path 指向扁平目录而不是真实会话树;startAtEnd 必须是 false。

配置里只能用绝对路径——~ 在这个文件里不会被展开,展不开的路径会让 watcher 在计算「通配符之前最深的那个真实祖先目录」时拿到空值,然后同样静默地什么都不做。

最后按这个顺序收尾:

mkdir -p "$HOME/.dsh/sessions-cmem"    # 先建目录
claude-mem restart                      # 再重启 worker
1
2

顺序反了的话,watch 会挂在一个不存在的目录上,静默失效且永不重试。


# 5. 会话格式:字段路径怎么核对

写侧还有一半工作量在 schema 上——告诉监听器 session.v3.jsonl 的每种记录长什么样。先看一眼你本机的实际类型分布:

python3 -c "
import json,collections
c=collections.Counter()
for l in open('$HOME/.dsh/sessions-cmem/<session-id>.jsonl'):
    c[json.loads(l).get('type')]+=1
print(c)"
1
2
3
4
5
6

预期输出(一个典型短会话):

Counter({'user/message': 4, 'agent/inbox/spliced': 2, 'step/start': 2, 'session/title': 2,
         'assistant/message': 2, 'step/end': 2, 'session': 1, 'permission/preset': 1,
         'sandbox/mode': 1, 'approval/policy': 1, 'turn/start': 1, 'system/message': 1,
         'request/header': 1, 'request/context': 1, 'tool/call': 1, 'tool/result': 1,
         'turn/end': 1})
1
2
3
4
5

十七种记录,只需要映射五种。几个反直觉的点:

用户消息不能按 type 匹配。 同样形状的记录也会出现在 agent/inbox/spliced 批次里,所以匹配条件应该是 data.source.kind == "user":

{"type":"user/message","seq":8,"data":{
  "content":[{"type":"text","text":"..."}],
  "source":{"kind":"user"},"role":"user","id":"..."}}
1
2
3

助手消息的 content[0] 往往不是回复。 它常常是一个 reasoning 块,回复在 content[1]:

{"type":"assistant/message","data":{"turn":1,"step":1,
  "message":{"role":"assistant","content":[
    {"type":"reasoning","text":"..."},
    {"type":"text","text":"..."}]}}}
1
2
3
4

所以取值必须先试 content[1].text、失败再回退 content[0].text。顺序反了的结果是:你的记忆库存的全是模型的思维链,而不是它给出的答案。这个错误不会报错,只会让你日后检索到一堆自言自语。

工具调用的 arguments 是字符串不是对象,调用与结果靠 callId / toolCallId 配对:

{"type":"tool/call","data":{"callId":"call_...","name":"mcp__claude_mem__search",
                            "arguments":"{\"query\": \"...\"}"}}
1
2

故意不映射的三种:request/header、request/context、session/title-llm-request。它们装的是每一轮发给模型的组装后完整请求——系统提示词、全部工具定义、以及重新序列化的整段历史。映射它们会让存储爆炸,还会把系统提示词逐字抄进记忆库。实测确认:不映射这三种之后,观察记录里没有任何系统提示词文本。


# 6. 八个不报错的失效点

这个集成从头到尾没有一个失效点会报错。全表:

现象 真正的成因
watcher 永远收不到任何会话 Bun 递归 fs.watch 不给 watch 启动后新建的目录挂 inotify;dsh 每会话一目录
watch 完全没动静,且无任何报错 worker 启动时被监听目录不存在,静默失效且永不重试
配置里的路径看着没错 配置文件里的 ~ 不会被展开,必须绝对路径
长会话进得来,短会话一条不进 startAtEnd: true,文件第一次被发现时已在末尾;而短会话被发布时早已停笔
watcher 读不到内容 dsh 默认把会话压成 .jsonl.zstd
dsh 每一次会话操作都抛异常 改明文后旧的 .jsonl.zstd 还留在 root 下——一个 session root 只能有一种编码
会话目录莫名搬家 dsh 的 patch 会整体替换目标行的 config,没重述的字段直接丢失
dsh 里根本看不到 MCP 工具 stdio 桥接按 /KEY\|PASSWORD\|SECRET\|TOKEN/i 与 DSH_* 洗掉环境变量;env 未显式声明

其中三条值得展开。

startAtEnd 为什么必须是 false。 直觉上「从末尾开始读」是对的——避免把历史文件重读一遍。但在这个架构下,被发布到扁平目录的每一个文件本身就是一段完整的、已经结束的历史。设成 true 的结果是:文件第一次被发现时游标就在末尾,此后它不会再增长,于是贡献零条记录。长会话偶尔能侥幸赶上追加,短会话则系统性地全军覆没。从 0 字节读起才是正确语义。

编码单一性。 把会话从 zstd 改成明文,只改配置是不够的:只要 root 下还留着任何一个 .jsonl.zstd,dsh 的 create / open / stat / list 全部会抛异常。切换前必须把整个旧目录挪走:

mv ~/.dsh/sessions ~/.dsh/sessions-zstd-archive-$(date +%Y%m%d)
mkdir -p ~/.dsh/sessions
1
2

patch 是替换不是合并。 dsh 的 patch 条目会把目标行的 config 整体换掉。所以改压缩方式时,必须把原本就有的 root 一并重述:

- id: session-persistence-jsonl
  config:
    root: !!js dshHomePath('sessions')   # 不重述这一行,会话目录就搬家了
    compression: none
1
2
3
4

# 7. 把每条坑变成一个会失败的断言

上面八条如果只是写在文档里,下次还是会踩。正确做法是每条对应一个可执行的断言,让「装没装对」变成一个可以跑出来的答案,而不是靠感觉:

Gate 0 — MCP server         握手成功、search 工具在列(顺带打印版本)
Gate 1 — 明文转录           存在 session.v3.jsonl,且 root 下无 .jsonl.zstd 混杂
Gate 2 — 发布器             扁平目录存在、已有产物、timer active
Gate 3 — watcher            path 指向扁平目录、startAtEnd 为 false、schema 已定义、
                            状态文件里有对应偏移量
Gate 4 — 数据真的落库       sdk_sessions 里有本平台的会话,且 observations 能 join 上
1
2
3
4
5
6

Gate 4 那两条 SQL 是最终裁决——前面全绿但这里为空,说明 schema 字段路径没匹配上:

DB=~/.claude-mem/claude-mem.db
sqlite3 "$DB" "select count(*) from sdk_sessions where platform_source='dsh';"
sqlite3 "$DB" "select count(*) from observations o
               join sdk_sessions s using(memory_session_id)
               where s.platform_source='dsh';"
1
2
3
4
5

完整脚本在 scripts/verify.sh (opens new window),任一 gate 失败即非零退出。


# 8. 隐私:默认采集了什么

转录里有你输入的一切,以及工具返回的一切——这是最宽的面:一次读文件把文件内容放进了转录,一条 shell 命令把它的输出原样放了进去。

会被读进记忆的:你的提示词、助手的可见回复、工具调用及其结果。不会被读进去的:系统提示词(在 system/message 与 request/header 里,两者都不映射)、每轮组装的完整请求、会话标题生成请求。

有一个反直觉的点值得单独说:dsh 会发出「合成的」用户消息——注入的 <system-reminder>、工作区 AGENTS.md 的内容、运行时上下文快照。它们带的也是 source.kind: "user",所以会被当成普通提示词读走。实际后果是你的工作区指令文件会被摘要进记忆。通常无害,但如果某个 AGENTS.md 里有不想被索引的东西,就要知道这件事。

对应的开关是在发布环节做排除——一个装满子串模式的排除文件,命中的会话根本不进扁平目录,也就永远到不了记忆库:

# ~/.dsh/cmem-export.exclude
/clients/
--some-workspace-slug--
1
2
3

排除在发布时判定。已经发布过的会话不会被追溯撤回,要停就删掉扁平目录里那个硬链接,并单独清理库里已有的记录。


# 9. 可复用要点

  1. 静默失效要靠断言发现,不能靠日志。 这个集成八个失效点零报错。凡是「配置看起来全对但没效果」的系统,唯一可靠的手段是为每个环节写一条会失败的断言,从最上游(server 能不能握手)逐段往下推。
  2. 文件监听不要假设递归就等于持续递归。 「recursive 为 true」在多数运行时里只承诺启动时的那棵树。只要被监听方是「一个任务一个新目录」的布局,就得先确认这一点,再决定架构——不然会浪费掉一整轮排查。
  3. 绕不开运行时限制时,改变输入的形状,而不是硬调参数。 后建目录看不见,那就别让它看后建目录:把完稿产物发布到一个稳定的扁平目录。多一跳、多两分钟延迟,换来的是可靠性从「随机」变成「确定」。
  4. 让 agent 自证时必须做阳性对照。 它会把一条坏掉的管道包装成「未找到相关结果」并附上自信的总结。任何「查了,没有」的结论,都要用一个必然命中的输入复跑同一条管道来验证。

# 10. 延伸阅读

完整可复现的实现开源在 Carry00/dsh-claude-mem (opens new window):安装器、验证脚本、配置模板,以及一份写给 AI agent 执行的作业手册(AGENTS.md)——把仓库交给任意编码 agent,说一句「读 AGENTS.md 装上」即可,路径全部探测而非写死。

本系列另外四篇:提示词分层与技能加载实测、三个 Harness 的解剖、五个核心心智模型、三处设计张力。


🤖 Agent 可直接解析的元数据块(点击展开)
{
  "_meta": {
    "doc_version": "2026-09-09",
    "article_id": "dsh-05-shared-memory-with-claude-mem",
    "profile_context": "any",
    "estimated_setup_time": "60min",
    "reference_repo": "https://github.com/Carry00/dsh-claude-mem"
  },
  "quick_start": {
    "step_1": "探测路径:CM_HOME / CC_CONFIG / CM_PLUGIN,优先取 marketplace 副本而非 cache 副本",
    "step_2": "用三行 JSON-RPC 直接向 mcp-server.cjs 发 initialize + tools/list,确认握手与版本",
    "step_3": "归档旧的 .jsonl.zstd 会话,patch 里设 compression: none 并重述 root",
    "step_4": "patch 里插入 MCP client 条目,直接指向 mcp-server.cjs,显式声明 env",
    "step_5": "建扁平目录 sessions-cmem,装发布脚本与 2 分钟 timer",
    "step_6": "watcher 的 path 指向扁平目录、startAtEnd 设 false、全绝对路径",
    "step_7": "先确认扁平目录存在,再重启 worker(顺序不能反)"
  },
  "silent_failure_modes": [
    "Bun 递归 fs.watch 不给 watch 启动后新建的目录挂 inotify —— 每会话一目录的布局结构性不可见",
    "被监听目录在 worker 启动时不存在 —— watch 静默失效且永不重试",
    "配置文件里的 ~ 不展开 —— 通配符祖先目录解析为空",
    "startAtEnd: true —— 短会话系统性全部丢失",
    "session root 混有 .jsonl.zstd —— dsh 所有会话操作抛异常",
    "patch 整体替换 config —— 未重述的 root 导致会话目录搬家",
    "stdio 桥接洗掉匹配 KEY|PASSWORD|SECRET|TOKEN 与 DSH_* 的环境变量 —— env 必须显式声明",
    "assistant/message 的 content[0] 常是 reasoning 块 —— coalesce 顺序错则存的是思维链"
  ],
  "safety_rules": [
    "切换压缩方式前先整体归档旧会话目录,一个 session root 只能有一种编码",
    "改 patch 时重述目标行原有的全部字段,patch 是替换不是合并",
    "重启 worker 之前先确保被监听目录已存在",
    "不要映射 request/header 与 system/message,否则系统提示词逐字进库",
    "开启前评估工具结果入库的范围:读文件与 shell 输出会原样进转录",
    "agent 报告『没搜到』一律做阳性对照后才采信"
  ],
  "verification": {
    "check_mcp_handshake": "printf 三行 JSON-RPC | node <CM_PLUGIN>/scripts/mcp-server.cjs,期望 serverInfo 与 tools 列表",
    "check_plaintext_sessions": "find ~/.dsh/sessions -name 'session.v3.jsonl' | wc -l  # 且 *.jsonl.zstd 计数为 0",
    "check_publisher": "systemctl --user is-active dsh-cmem-export.timer  # active;且扁平目录非空",
    "check_watch_offsets": "读 watcher 状态文件,确认存在指向扁平目录的字节偏移量",
    "check_db_ingested": "sqlite3 <db> \"select count(*) from observations o join sdk_sessions s using(memory_session_id) where s.platform_source='dsh';\"  # 大于 0"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

AI Agent 使用指南:

  • 当用户说「记忆库一条都没进,但配置是对的」→ 按 silent_failure_modes 前三条依次排除:监听的是不是后建目录、目录在 worker 启动时存不存在、路径有没有用 ~
  • 当用户说「长会话有记录、短会话没有」→ 直接查 startAtEnd,应为 false
  • 当用户说「dsh 里看不到 MCP 工具」→ 查 env 是否显式声明,以及是否误指向了启动器而非 mcp-server.cjs
  • 当用户说「存进去的都是模型自言自语」→ 查 assistant/message 的 coalesce 顺序,content[1].text 必须先试
  • 当用户说「帮我把这套装上」→ 按 quick_start 逐步执行,每步跑对应的 verification 项,全绿再报完成
#AI Agent#Agent架构#MCP#DeepSeek Harness
上次更新: 9/8/2026

← DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力

最近更新
01
DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力 原创
09-08
02
DeepSeek Harness 实战 03|学习笔记:读 12 篇 README 拿下 dsh 的五个核心心智模型 原创
09-08
03
DeepSeek Harness 实战 02|三个 Harness 的解剖:同一个文件名,三种加载语义 原创
09-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式