DeepSeek Harness 同一条用户消息出现两个气泡:排队回显与落盘各渲染一次的排查
「好了吗」这条消息,在对话流里出现了两个一模一样的气泡,时间戳都是 00:17,还各带一个复制按钮——看着就像发了两次。 但会话日志里那次发送只有一条真实的用户消息,其余都是自动注入的上下文(#10808)。把「数据层」与「显示层」分开数,这件事就不神秘了。
DeepSeek Harness 第一层:数据层确实只有一条
日志里 source.kind === "user" 的事件只有一条,其余同角色事件都不是你写的。
- 打开会话日志
session.v4.jsonl.zstd,定位那次发送的时间点。 - 能看到四条
user/message:kind=user是「好了吗」。 - 另外三条分别是
kind=memmy-memory、kind=runtime-context、kind=time-context。 - 后三条
role也是 user,但它们不是用户输入,是 cadence 自动注入的。 - 所以数据层干净:一条输入,不会重复计费,也不会重复回答。
00:17:41 user/message kind=user "好了吗"
00:17:41 user/message kind=memmy-memory "<memmy_l3_world_model ..."
00:17:41 user/message kind=runtime-context "Current runtime context ..."
00:17:41 user/message kind=time-context "Time sampled ..."
四行同一个时间戳,但只有第一行是你发的。这一点决定了后面所有判断:气泡数量不能用来推断消息条数。
DSH插件 第二层:显示层为什么变成两条
排队回显被从 transcript 里隐藏,但它又被排除在去重集合之外,于是被当成独立气泡渲染。
- 前端维护一份待提交账本
pendingSubmissions,记录每条乐观显示消息的位置。 - 去重集合
localIds的来源是:pendingSubmissions.filter(s => s.placement !== "queued").map(s => s.requestId)。 - 注意这个过滤条件——queued 被排除在集合之外。
- 另一侧,
visibleSubmissions把 queued 的提交从 transcript 里隐藏掉。 - 但在
inboxSteering的渲染逻辑里,只要source.kind === "user"有rpcId,就会去localIds里找。 - 找不到(因为 queued 不在集合里)→ 直接渲染这一条。
- 等消息落盘后,transcript 里再渲染一次 → 两个气泡。
DeepSeek Harness 第三层:为什么只有排队时出现
Agent 空闲时发送走的是下一轮路径,入队与出队几乎同时发生,你看不到重复。
- 报告者记录的事件链:Agent 运行中发送时,先是
agent/inbox/spliced目标是next-step。 - 随后还有一次 splice 带
removedCount: 1。 - 之后才出现
user/message kind=user。 - 另一次在 Agent 空闲时发送,走的是
next-turn,入队与出队同时发生,只看到一条气泡。 - 结论:触发条件是「消息进入排队路径」,与插件无关——报告者逐个排除过第三方插件。
DeepSeek插件 第四层:为什么不用担心数据层
这只是显示层多画了一次,会话内容、模型调用与计费都不受影响。
- 模型只读到一条用户消息。
- 复制按钮出现两次,但复制的内容相同。
- 刷新或重开会话后,气泡数量应回到一条。
- 因此它是体验问题,不是数据完整性问题。
- 报告里也明确写了「数据没问题,是前端把同一条渲染了两次」。
DeepSeek Harness 版本状态与修法
两个提交修掉了这条路径,但它们都在 0.2.2-alpha.2,不在 rc.4。
- 第一个提交
ae07093132d:让 transcript 在回合结束与 steering 落地时保持稳定。 - 其中一条说明是「排队回显等待 Inbox 投影,而不是在持久化 splice 时退休,这样队列停靠区不会空一帧」。
- 第二个提交
a1659120a38:当排队回显的 Inbox 行被取消时,把回显撤掉。 - 两个提交都改了会话状态文件与对应的待提交测试。
- 都在
f3803c96(0.2.2-alpha.2)里,不在504a01a9(rc.4)里。 - 数据层不需要改,日志格式也不用动。
DSH插件 怎么确认自己遇到的是这一条
三个特征同时成立就对上了:内容逐字相同、时间戳相同、日志只有一条 kind=user。
- 对比两个气泡的正文,是否完全一致(包括标点)。
- 对比时间戳,是否精确到同一分钟。
- 去日志里数
kind=user的user/message条数。 - 回忆发送时机:是不是在 Agent 正在跑工具调用时发的。
- 换到 Agent 空闲时再发一条,看是否只有一条气泡。
注意事项
- 别按「重复发送」去查网络。日志已经证明只有一条。
- 别删会话重来。刷新或重开就能恢复一条的显示。
- 别把自动注入事件当成用户消息。它们
role也是 user。 - rc.4 上仍会出现。修复只在 alpha 线。
- 报问题时附时间戳与日志条数。这是区分显示问题与真实重复的最快证据。
如果你的两个气泡内容略有差异,那可能是两条真实消息,逻辑完全不同,先去核对日志。
反过来,如果日志里确实有两条 kind=user,那就是真的发了两次,得去查输入通道而不是渲染。相关排查见 DeepSeek Harness 会话显示异常:标题不生成、只跑命令显示空白与删缓存丢会话。
常见问题
不是,DeepSeek Harness 只把这条消息发送了一次。会话日志里那次发送只有 1 条 source.kind === "user" 的事件,其余是 cadence 自动注入的 memory、runtime-context、time-context 事件。两个气泡是纯前端显示问题。
因为这种发送在 DeepSeek Harness 里走排队路径(placement === "queued")。排队回显会被隐藏于 transcript,但同时又被排除在去重集合之外,于是队列里那条被当成独立气泡渲染;消息落盘后再渲染一次,就成了两个。
不会,DeepSeek Harness 的数据层只有一条用户消息,模型只处理一次。这个问题只影响显示,不影响会话内容与费用。
DeepSeek Harness 的这个显示问题由两个提交 ae07093132d 与 a1659120a38(都是 10-27,rc.4 之后)修掉,它们都在 0.2.2-alpha.2 里,不在 rc.4 里。所以 rc.4 上双气泡仍会出现。
在 DeepSeek Harness 里看两个气泡的时间戳是否完全一致、内容是否逐字相同,再去会话日志里数 kind=user 的 user/message 只有一条。两者同时成立,就是显示层重复。
相关术语
- queued placement
- queued placement 是消息在 Agent 运行中提交时采用的排队位置标记。它让消息先进入队列,等当前回合结束或下一步开始时再并入对话。— https://github.com/deepseek-ai/deepseek-harness/discussions/10808
- pendingSubmissions
- pendingSubmissions 是前端维护的待提交消息账本,记录每条乐观显示的提交及其位置。去重逻辑会从这里取 requestId 组成集合。— https://github.com/deepseek-ai/deepseek-harness/discussions/10808
- localIds
- localIds 是渲染时的去重集合,取自待提交消息的 requestId。当前实现把 queued 的提交排除在外,因此排队那条不会被去重。— https://github.com/deepseek-ai/deepseek-harness/discussions/10808
- agent/inbox/spliced
- agent/inbox/spliced 是会话事件,表示一条输入被插入到当前步骤或下一回合。它携带目标位置与移除计数,可用来判断消息走的是排队还是下一轮路径。— https://github.com/deepseek-ai/deepseek-harness/discussions/10808
来源
- #10808 — 发送消息后同一条用户消息出现两个气泡(数据层只有一条)· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness(源码仓库)· GitHub