Repository navigation
[Bug] 每轮「改动文件」卡片会静默丢失,且同一工作区并发会话会互相串入对方的改动 #11857
Unanswered
dongshan1999
asked this question in
Q&A
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
[Bug] 每轮「改动文件」卡片会静默丢失,且同一工作区并发会话会互相串入对方的改动
摘要
@deepseek-ai/dsh-workspace-changes+@deepseek-ai/dsh-client-ui-deliverables这套「改动文件卡片」机制有两个可复现的缺陷:两者都不是偶发,我在一个真实使用环境(约 810 个会话的历史日志)里做了量化:会话级零卡片率 15.9%(主会话)、轮次级缺失率 6.2%~23.3%、会话最后一轮缺失率 54.5%;原生事件中 24.7% 的轮窗口内混入了其他会话的写入。
经完整代码走查与第三方插件排除,判定这是框架层问题,不是任何第三方插件造成的(排除过程见文末)。
环境
app.asar)xcode-select -p已正确配置(排除 A7 退化路径)developerTools: true(即showCodeDiff总开关为开启状态)@deepseek-ai/dsh-workspace-changes0.2.1-rc.5、@deepseek-ai/dsh-client-ui-deliverables、@deepseek-ai/dsh-sessiongit rev-parse --is-inside-work-tree= true)复现步骤
缺陷 A:并发会话串台(必现,不需要特殊条件)
lib/foo.dart)。turn/start到turn/end)之内,让 B 开始一轮并在 A 写完之前结束;B 自己一个文件都不改。实际行为:B 的卡片里出现了
lib/foo.dart,甚至出现 B 从未访问过的目录下的文件(例如 A 持续写入 31 个文件,B 的卡片就把这 31 个都列上)。期望行为:B 的卡片应为空或只包含 B 自己写过的文件。
本机统计(原生事件 974 条,按精确的
turn/start.time → turn/end.time窗口判定):**241 条(24.7%)**窗口内存在别的会话写入、且那些路径本会话全生命周期从未写过;其中 23 轮本会话在该轮一次都没写过文件——即完全纯混入。缺陷 B:卡片静默丢失
实际行为:卡片消失;前端向
/api/changes.summary请求得到404 "Change summary unavailable.",不显示任何内容。期望行为:历史轮次的改动摘要应当可以重新拉取,或在 UI 上明确区分「已不可用」与「本轮无改动」。
另一条丢失路径(无需重启):工作区快照耗时接近但未超时的边界场景下,整轮被丢弃且无任何提示——见下文 A4/A5。
根因分析
一、串台:归属判据根本不存在
采集器每轮做两次整棵 git 工作树快照,再求差:
lib/types/git.jssnapshotTree():mkdtemp临时 index → 复制仓库.git/index→git add --all --ignore-errors→git write-treerecord()再拍一次,diffTrees(before, after)用git diff-tree -r -M -z --numstat求差cwd固定为workspace.root(由git rev-parse --show-toplevel得出)关键在于 diff 的对象是整个工作树,而不是本会话触碰过的路径。
recorder.js里唯一的归属相关逻辑是capture()(挂fs/write-intent/fs/edit-intent),但它只在 git 未覆盖某文件时补位,无法把别人的改动排除掉。也就是说,归属完全由「时间窗」决定:谁的轮窗口盖住了该文件的变化时刻,就记到谁头上。项目 README 里也写明了这个语义:
在单人单会话时这是可接受的近似;一旦同 cwd 并发(本机是常态),就必然串台。本机统计:同 cwd 且轮窗口重叠的会话对 760,其中交集区间内双方都实际写了文件的 532 对,涉及 240 个会话、8 个工作区。
二、丢失:三条独立且全部静默的路径
A4 — 基线快照失败即整轮放弃,且无任何用户可见提示
lib/types/recorder.jsstart(turn)里基线快照catch后:随后
record()第一步就早退:这意味着连
capture()已经收集到的文件工具调用记录也一起丢弃——明明知道改了哪些文件,却因为一个快照失败而整轮不报。失败本身只经warnUnlessDisposed打一行 logger 警告,不写事件、不进 UI、不重试。A5 — 失败标记先于实际工作置位,导致永不重试
record()中state.attemptedAfterSeq = state.lastToolResultSeq;写在快照之前。于是本轮一旦抛错,turn/end的补记判断永远成立,该轮永久丢失,没有第二次机会。把置位挪到成功之后即可修复。
B1/B2/B3 — 摘要只活在 Host 进程内存
事件里只存
{ turn },不含 files:'workspace/changes': { turn: number },本机 1240 条事件中data的键集合 100% 只有('turn',),含 files 的 0 条。摘要存于
byIdMap(index.js的ctx.provide("workspaceChanges", { summary: (id, seq) => byId.get(id)?.summary(seq) })),由路由dsh-client-ui-deliverables/lib/index.js的handleChangesSummary读取:ctx.on("session/disposed", session => forget(session))→forget()执行recorders.delete(session); byId.delete(session.id); recorder?.dispose(),而dispose()里records.clear()。records.clear()甚至是同步执行的,发生在 await 之前。客户端拿到 404 后:
const changes = ... typeof summary === "object" && summary.files.length > 0 ? {...} : null;→ 字符串"missing"不是 object → 无卡片。且ChangesSummaryStore.retryable: () => false,只有connection/reset才会清缓存 → 永久不重试。代码注释本身就承认了这个设计:
对照证据:同一框架里的
deliverables/presented事件把files[]写进事件,重启后照样能渲染(本机 85 个会话有此事件)。这证明「事件自带文件列表」在本框架里是完全可行的,workspace/changes只是没这么做。三、性能边界会放大上述问题
snapshotTree的成本是整个工作树,且临时 index 从不写回仓库,所以每次都是冷启动全量 hash。我在一个未提交、无.gitignore、含 5 个 embedded repo 的工作区上实测:git add --all --ignore-errors:15.5s,写入 548MB 对象turn/start+agent/turn-stopping)→ ≈31s/轮timeoutMs默认 30000(AbortSignal.timeout是每条 git 命令 30s,不是整轮)余量只有 1.9x。我实测 3 个并发快照各 18.1s(只慢 17%),所以不是必然超时,但机器繁忙/磁盘抖动时很容易踩线,一旦踩线就落入 A4/A5 的「整轮静默丢失」。
另外
agent/turn-stopping的处理是await recorders.get(agent.session)?.stopping(turn)—— 快照会阻塞轮末,这也解释了为什么这类工作区里轮次收尾明显变慢。影响面(脱敏后的实测数据)
会话日志全库 810 个会话 / 1240 条
workspace/changes事件。统计口径:某轮有write/edit调用且file_path落在 cwd 内,但日志中没有对应data.turn的workspace/changes事件。eligible()设计使然,属预期)按工作区(主会话):
注意 workspace-b / workspace-c 都是健康的、有 .gitignore 的小仓库,缺失率反而更高——说明这不是「仓库太大导致超时」的单一问题,A4/A5/B 系列才是主因。
整场 0 卡的样本(v4 主会话):27 个
turn/start/turn/end完整配对、498 次工具调用、10 个写轮共 63 次write/edit,而 3519 条事件里workspace/changes= 0,但同会话deliverables/presented= 2。事件通道和前端都正常,唯独缺这一路。逐轮失效的样本也能定位到具体轮次(例如 7 个写轮缺 4 个、27 个写轮只缺 1 个且该轮有 18 次写入),说明不是「整个会话被跳过」,而是特定轮的快照/记录失败。
已做的第三方插件排除
该环境装有多个第三方 DSH 插件(Host 侧与 Client 侧均有),已逐一排查:
tools/pre-execute0 命中、fs/write-intent0 命中、fs/edit-intent0 命中 → 插件根本没有接入采集链路。workspace/changes/workspaceChanges/changes.summary做全量 grep,在插件目录中零命中,生产者与消费者只有上述两个@deepseek-ai包。session.append的插件只有两处(均为往自己的会话补一条消息);挂agent/turn-stopping的只有一处且是 fire-and-forget。uiConversation/conversation.chat.turnTail/tool.call.toolview,无法隐藏卡片;各插件的 slot 用key:id唯一注册,不占用changes-review位。session?.id !== sessionId之类的守卫。因此这两个现象与第三方插件无关。
附:验证用的最小命令
All reactions