本地提交回显在持久节点落地前就被退役 —— 任何耗时的 agent/pre-step 监听器都会变成可见的消息延迟
#7167
Replies: 2 comments
|
补充一点更直接的证据,以及一个端到端的对照实验。 那个函数的注释已经写明了它该干什么
/** Retire echoes whose prompts landed in the host inbox instead of the log
* (running-turn submissions). */
observeSubmissionQueue(items) {
if (this.submissionSettlements.size === 0) return;
for (const item of items) if (item.rpcId !== void 0) this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));
}注释把范围限定在 running-turn submissions(提交那一刻这一轮已经在跑的那些),实现却没有检查 所以这里不只是「没做到 no gap」,而是实现偏离了它自己声明的适用范围。前面建议的修法,就是把它收敛回注释所述的范围: const queuedEchoes = new Set(this.pendingSubmissions.filter((echo) => echo.placement === "queued").map((echo) => echo.requestId));
for (const item of items) if (item.rpcId !== void 0 && queuedEchoes.has(item.rpcId)) this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));端到端对照实验为了确认这个洞在真实产品里的量级,我们做了一个单变量对照:同一个桌面壳实例、同一个正式版二进制、未打任何核心补丁,只切换那个在
关掉之后 pre-step 上没有耗时监听器,空窗缩到一个网络往返的量级,肉眼不可见。 关于严重程度这也说明了这个问题的量级:默认情况下这个洞是潜伏的 —— 它的宽度就等于 pre-step 的总耗时,没有慢监听器时小到看不出来。所以这不是「每条消息都必然丢 1 秒」,而是「任何把耗时逻辑放到 pre-step 上的插件,都会把它直接翻译成用户可见的消息延迟」。 我们之所以撞上,是因为那个桌面壳默认就装着一个在 pre-step 上跑 |
|
结论:在 HEAD 上成立。但有一处更正: 1. 三处决定可见性,其中两处键在「队列出现」而不是「持久节点」
两处都通过 2. 队列出现不是会话节点: 3. 持久节点不可能更早落地:它只在首次尝试时、在 ⇒ 顺序是:inbox splice 插入(持久,上一帧闩死退役)→ 4. 你可以这样自证(你正文里的复现思路是对的,这里给最小形态):挂一个插件 回显会在提交后约一帧消失,而消息气泡约 1 秒后才出现。 5. 修法形状(要两层一起改):只在带该 rpcId 的持久 6. 今天的用户侧绕过:没有。 别把慢活放在 诚实边界:没有在浏览器里跑,也没量真实帧时序。另外我没能确认你引的 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
现象
发送消息后,自己刚发出的那条消息要等 0.5–2 秒才出现在聊天记录里;在这段时间里,聊天区只显示 turn 状态指示器。消息不会丢,只是晚到。
这个窗口的宽度等于
agent/pre-step瀑布上消耗的总时间。机制
ISession.beginSubmission()会同步创建一条本地提交回显,它存在的唯一目的就是:不管下游发生什么,你自己发的消息都应立刻可见。有两处把「prompt 送到了 host inbox」当成「持久节点已经渲染出来了」,从而破坏了这个不变量:@deepseek-ai/dsh-api-session-controller——observeSubmissionQueue()一旦在队列镜像里看到该 rpcId 就调用scheduleObservedRetirement(),这会把回显从pendingSubmissions里移除。@deepseek-ai/dsh-client-ui-chat——observedRpcIds()里有for (const item of queue) if (item.rpcId !== void 0) observed.add(item.rpcId),而ChatView会把requestId落在该集合里的回显全部过滤掉。但队列行(
inbox.nextTurn)不是聊天记录里的节点。持久化的user节点只能在agent/pre-step瀑布返回之后才追加 —— pre-step 可能返回reject或改写这一批消息,所以宿主无法更早地落盘。结果就是:回显已经消失,持久节点还没到,留下一个宽度恰好等于 pre-step 耗时的空窗。用户观察到的顺序也由此直接推出:
turn/start把running置为真,于是TurnStatus先渲染,消息气泡要等持久节点落地才出现。observedRpcIds自己的文档注释写着这个交换是atomic — no duplicate, no gap,这显然就是设计意图。队列来源那两行正是制造 gap 的地方。最小复现
不需要桌面端,也不需要任何插件。在任意插件里:
然后发一条消息。它会晚大约一秒出现,而这一秒里聊天记录只显示 turn 状态指示器。
真实案例:
dsh-tauri-turnrewind在agent/pre-step的 step 1 上跑一次工作区快照(git add --all→write-tree→ls-tree -r -l)。在一个 1223 个文件的工作区上,我们实测冷启动 2.77 秒、稳态约 1.1 秒,几乎精确复现了报告中的 0.5–2 秒延迟。建议的修法
只有持久的
user/steering节点才应算作 observed。队列来源只应退役「提交时这一轮已经在跑」(placement === "queued")的那些回显 —— 那类回显本来就不在聊天记录里渲染,因为队列条接管了它们,而队列条已经按 rpcId 与宿主队列行做了去重。具体来说:
observeSubmissionQueue:把退役条件收紧到回显自身的placement === "queued"observedRpcIds:整个删掉队列来源(ChatView已经有placement !== "queued"这个条件了)两处都必须改。只改 store 层,回显虽然还在
pendingSubmissions里,但会被渲染层过滤掉;只改渲染层,store 层早就把它删掉了,渲染层没东西可渲染。已知代价
如果一条消息在提交的瞬间恰好被排进了队列,它会在很短一段时间内(约等于 pre-step 的耗时)同时出现在聊天记录的回显和队列条的宿主队列行里。这是「让回显活到持久节点落地」无法避免的代价。上游目前是靠隐藏聊天记录那一份来避免重复的,而正是这个隐藏制造了空窗。
背景
我是在
dsh-tauri/deepseek-harness-desktop(一个打包了 dsh 的桌面壳)上遇到这个问题的。我们目前把它作为一个幂等补丁,在启动时打到活动核心上,症状完全消失。如果相比下游补丁你们更希望走 PR,我很乐意直接对 core 提一个。All reactions