生命不息,折腾不止。今天教你读懂 dsh 那张「只增不改」的会话日志——Agent 每一步干了啥、烧了多少 token、哪里跑偏了,全都有据可查。

一、先搞清楚一个核心:dsh 的会话是「只追加」的账本

很多人用 DeepSeek Harness(dsh)跑 Agent,跑完就完事了,从没想过「它刚才到底怎么想的」。其实 dsh 从底层就把每一次会话都当成一本只追加(append-only)的账本在记——这正是官方反复强调的那句「Every run is traceable(每次运行都可追溯)」的底气所在。

什么意思?你发一句话,Agent 思考、调工具、改文件、回结果,这一连串动作会被拆成一条条事件(event),按顺序(seq 从 0 连续编号,events[i].seq === i)追加进日志里。关键点在于:

  • 只追加、不改写:历史事件写进去就永远在那里,不会被覆盖。这跟 git 的 commit 是一个思路,所以它天然支持「回放」「分叉」「回溯」。
  • resume、fork、search、replay 全都操作同一条事件流:这是 dsh 官方原话。恢复会话、从中间分叉、搜索历史、回放,本质都是在这本账本上做文章,而不是另起炉灶。

这套设计在工程上叫「事件溯源(event sourcing)」。好处是:只要日志还在,任何时刻的会话状态都能重建——哪怕程序崩了、界面卡了,账本没丢,事情就没白干。

二、日志到底存在哪、长什么样

日志默认落在 ~/.dsh 目录下(可以用环境变量 DSH_HOME 改位置)。每个会话一个独立目录,核心文件是压缩过的 JSONL:

1
2
3
4
5
~/.dsh/sessions/
<项目目录>/ # 归一化后的工作目录
<会话id>/
session.jsonl.zstd # zstd 压缩的只追加 JSONL(当前格式)
session.jsonl # 关掉压缩时就是裸 JSONL

版本迭代比较快,你可能会看到 session.v2.jsonl.zstd、session.v3.jsonl.zstd 这类带版本号的变体,不影响理解——最新的那份就是当前会话日志。具体布局以官方文档为准。

想亲眼看看账本长啥样?机器上装了 zstd 的话,直接解压读:

1
2
# 找到某个会话的日志文件后,解压看原始事件流
zstdcat ~/.dsh/sessions/<项目>/<会话id>/session.jsonl.zstd | head -5

你会看到第一行是一条 SessionHeader(会话头),记着会话 id、创建时间、工作目录 cwd、父会话 parentSession、seedLength、agentPreset 等元信息;后面每一行就是一条事件,比如 turn/start(一轮开始)、step/start(一步开始)、工具调用、assistant/message(模型回复)、session/end-seed(分叉/恢复的边界)…… 串起来就是 Agent 完整的一生。

三、内置 Trajectory 视图:不用碰文件也能看全过程

命令行读日志有点硬核,日常调试其实靠 Web UI 内置的 Trajectory(轨迹)视图就够了。你在会话页顶部切到 Trajectory 标签,能看到一个按轮次/步骤展开的树,每层都有:

  • 模型想了什么、回了什么(思考过程 + 最终回复)
  • 调用了哪些工具、传了啥参数、返回了啥结果——嵌套的子工具会以 SUBTOOL: xxx 的形式标出来,一眼看出它是自己内部又去调了什么
  • 每一步的耗时和 Token 用量:模型时间、工具时间、首 Token 延迟、解码时间,都能在这里对得上

这玩意儿调试 Agent 特别实用。Agent 跑偏了,别再对着结果瞎猜,直接进 Trajectory 看它在第几步、调了哪个工具、拿到了什么结果才走岔的。我自己的经验:90% 的「Agent 犯蠢」都能在这棵树里定位到具体那一步。

**恢复(resume)**也在这条链上:重启 dsh 后,左侧会话列表里点开旧会话,日志会被恢复、上下文原样接上,你可以让它接着上回没干完的活继续。**分叉(fork)**则是从某个历史节点开一条新枝,新会话会记录 parentSession 指向老会话——「我先这么试一条路,不行再回头走另一条」,就是这么来的。

四、进阶:装个插件,把日志变成「时光机」

内置 Trajectory 能看单会话的步骤,但社区插件能把账本玩出花。推荐两个我用过觉得值的:

① @mingozhou/dsh-replay——会话时光机

一句话:把 append-only 日志变成可交互的回放。装上之后侧边栏会多一个 Session Replay 入口,每个会话里也多一个 Replay 标签,能:

  • 时间线回放:按 1–16 倍速拖动回看每一轮、每一步、每条工具调用,点任意事件看原始 prompt / 参数 / 结果 / 耗时
  • 逐步 token 账单 + 成本估算:累计 token 曲线、工具耗时排行,还能按 DeepSeek / Claude / OpenAI / Gemini 的价目表估算这一会话大概花了多少钱(价目表可改)
  • 安全审计:规则化扫描危险操作(rm -rf、sudo、curl | sh 之类)、敏感路径(.env、~/.ssh)、权限/沙箱变更、被拒绝的审批,按严重程度排好、可点击下钻
  • fork 血缘树:整条会话的血缘关系画成可点树,分叉边界标得清清楚楚,子 Agent 单独标记
  • 双会话对比:任意两个会话并排比 token 用量、工具组合,还能精确标出它们从哪一条事件开始分道扬镳
  • 一键导出 HTML:把整个会话烤成一个离线 .html,发给队友或贴进 bug 报告,对方零安装就能看完整回放

装起来一条命令(带 dsh CLI、用 profile 方式安装):

1
2
3
4
5
dsh plugin --profile web add @mingozhou/dsh-replay
# 验证装上了
dsh --profile web --dump-config # 应看到 @mingozhou/dsh-replay 段
# 重启后打开 http://127.0.0.1:3080
dsh web --profile web

插件作者在 GitHub 上放了零安装的在线 demo(https://mingozhou.github.io/dsh-replay/ ),不想装可以先点进去体验三份样例会话。

② dsh-retrace——撤回 / 编辑重发 / 重新生成 + 版本回退

dsh 的日志只追加、本身没有「撤销」。dsh-retrace 补上聊天本该有的三个操作,再往前一步做版本化:每次回退记成一个版本,有时间线、有产物文件回退(git 优先 + 快照兜底),还能一键跳回对话的任意位置。注意它的哲学:撤回删掉的是「视图和上下文」,底层日志永不改写——它只是追加一条合法的替换事件把对话表面回退掉,审计痕迹全程保留。

1
dsh plugin add dsh-retrace

五、几个实操心得(含踩坑)

折腾下来,这几个点值得记:

  1. token 账单能帮你核对成本。如果你接的是中转站(比如 ai.aklibk.com 这类按量付费的),配好价目表后 dsh-replay 的「成本估算」就能直接跟你后台的账单对一下,Agent 跑一晚上的钱花哪了、哪个工具最烧 token,一目了然。省得月底看账单一脸懵。

  2. seq 连续性 = 日志健康度。健康日志的 seq 是连续无缺的(events[i].seq === i)。如果你自己写脚本解析日志,发现 seq 跳号,多半是多进程并发写同一会话日志了——这属于已知坑,别在多个进程里同时 append 同一个会话。

  3. 冷启动空白是已知 bug。重启 dsh 后点开旧会话,对话区偶尔空白(数据其实在),切到 Trajectory 再切回来就正常了。不是日志丢了,别慌着去删数据。

  4. 流式输出是「打包 chunk」存的。为了省空间,连续的流式增量会打包成 text-chunks / reasoning-chunks / tool-call-chunks 这类行(带 seq0 + 每个成员的时间差 dt),解析时注意解包,别把一条当一条真事件读。想自己写工具直接 import dsh-replay/core,它已经帮你处理好了打包行、崩溃截断尾、分叉边界这些脏活。

  5. 日志 = 账本,但记得别乱删。~/.dsh 会越积越大,清理前想清楚:删了账本就意味着那段历史彻底没了,resume/fork/回放全都失效。要省空间优先删旧的、已经收尾的会话,别动正在用的项目目录。

六、写在最后

「把 Agent 每一步都录下来」这事,看着是给调试用的,其实它是 dsh 整个框架的地基——因为日志是只追加、可重建的,所以才能做分叉、回放、恢复、审计这些高级玩法。搞清楚这本账本,你对 dsh 的理解会从「会用」直接跳到「能诊断」。

生命不息,折腾不止。下一期我准备讲 《DeepSeek Harness 无头自动化:不开浏览器,用命令行让 Agent 自己跑批量任务》——很多兄弟想让它半夜自动干活、或者塞进脚本里流水线化,下一期就带你玩 dsh 的 CLI 模式和无头跑法,把「人工守着点」变成「写完睡觉,明早收结果」。