Repository navigation
[BUG] 0.4.1:新建会话时 reconcile() 在会话追加窗口内重注册 ops_bash,register 全部被宿主拒绝(session append cannot reenter),新会话拿不到 ops_bash #386
Description
摘要
lib/session-shells.js 的 reconcile() 由 agent/created(attach())同步触发,而 agent/created 发生在宿主正在追加新建会话事件的窗口内。clear() → entry.dispose?.() 与 publish() → entry.ctx.tools.register() 都要写会话事件,因此这一次全表重建整批被宿主拒绝:
dsh-ops: bash tool teardown failed; continuing with the remaining sessions: session append cannot reenter while another append is being published
dsh-ops: bash publication failed: session append cannot reenter while another append is being published
dsh-ops: bash restoration failed: session append cannot reenter while another append is being published
两个后果:
- 新建会话当下拿不到
ops_bash:publish()的 fix: 修复 Linux 上原生 worker 完全不可用,并补上四条关停路径 (#26) #30 兜底(用entry.definition再注册一次)同样被拒,新建那条 entry 的definition还是undefined,于是直接 return,要等下一次reconcile()才补上。 - 点「新建会话」的延迟随存活 agent 数线性增长:每个已注册过 bash 的存活 agent 会留下 3 行 warn(teardown / publication / restoration),也就是 2 次白烧的追加;实测每行 ≈ 47 ms。
与 #382、#333 是同一触发点的不同失败模式:#382/#333 讨论的是「全表重建的 O(N) 成本」(这些重建在你们机器上大概成功了),这里报的是「重建整批失败 + 重试仍失败 + 新会话因此缺工具」。即使按 #382 的建议把它改成增量,只要
register仍在追加窗口内同步调用,新建会话自己那一次发布还是会撞上同一个拒绝。
dsh-ops 版本与安装通道
dsh-ops0.4.1(npm 安装,即当前dist-tags.latest),装在~/.dsh/profiles/desktop/node_modules/dsh-ops,未做任何本地修改。- 采集完这份数据后,用户已在市场里卸载 dsh-ops(market log:
uninstall exit=0 live-removed=false),所以下面是卸载前在同一 Host 进程内采集的原始数据。需要我复装再抓一份 CDP 时间线的话随时说。
DSH 版本与 profile
- DSH Desktop(DSH Next 安装包),Host 自述
dsh 0.1.7(市场插件在this host's settings service has no register() (dsh 0.1.7 …)里引用) - Electron 44.0.0 / Chromium 152.0.7977.54 / 内嵌 Node v24.18.1
- profile
desktop,dsh.profile.bundles12 项(dsh-ops 在列) - 插件权限模式:
workspace-write(走allowed(agent)的sandboxPolicy.resolve分支,即publish()的正常路径)
操作系统与架构
Windows 10 22H2(10.0.19045),x64,i7-9700。
用户可见症状
「新建会话」点击后要 1–3 秒才响应,而且同一 Host 进程里会话越开越多就越慢,重启 DSH 立刻恢复。宿主日志里每次点「新建会话」都成片出现上面那三行 warn。
量化(同一 Host 进程内,不重启)
测量方法:读 ~/.dsh/sessions/<workspace>/<session>/session.v4.jsonl.zstd,取第一帧里 session 事件的 createdAt,减掉该文件的 mtime;「warn 行数」是同一秒内 dsh-ops 的 warn 条数。
| 时间 (UTC) | warn 行数 | 新建会话耗时 |
|---|---|---|
| 09:46:41 | 24 | 1 225 ms |
| 13:40:24 | 30 | 1 564 ms |
| 13:41:01 | 36 | 1 798 ms |
| 13:41:03 | 39 | 2 041 ms |
| 15:00:44(重启后,存活 agent 只剩 2 个) | 6 | 381 ms |
斜率 ≈ 47 ms / 行,也就是每个存活 agent ≈ 150 ms,全部落在点击「新建会话」这一下里;重启后线性项归零。同一天的 warn 集群行数单调爬升:9 → 12 → 15 → 21 → 24 → 30 → 36 → 39。
时序上也能确认它就在创建窗口里(createdAt 与首条 warn 相差 6–10 ms):
13:41:01.537 session 事件 createdAt
13:41:01.543 dsh-ops: bash tool teardown failed … session append cannot reenter …
13:41:01.573 dsh-ops: bash restoration failed … (本轮共 36 行)
根因(行级,0.4.1)
// :211 agent/created 是同步回调,此时宿主正处在新建会话的 append 里
ctx.on('agent/created', ({ agent }) => { attach(agent); });
// :185-196
const attach = (agent) => {
...
entry.fiber = agent.ctx.plugin(child => {
child.inject(['tools'], toolsCtx => {
entry.ctx = toolsCtx;
reconcile(); // ← 同步全表重建
});
});
};
// :161-184
const reconcile = () => {
for (const entry of entries.values()) clear(entry); // clear() → entry.dispose?.() → 写会话事件(被拒)
for (const [agent, entry] of entries) { ... publish(entry, definition); } // register() → 写会话事件(被拒)
};
// :139-160 publish 的兜底在同一次同步调用里再注册一次,仍然被拒
catch (error) {
ctx.logger?.warn?.(`dsh-ops: bash publication failed: ${errorMessage(error)}`);
if (entry.definition === undefined) return;
try { entry.dispose = entry.ctx.tools.register(entry.definition); }
catch (restoreError) { ctx.logger?.warn?.(`dsh-ops: bash restoration failed: …`); }
}宿主的拒绝文案是 session append cannot reenter while another append is being published(同一个 session 的 append 不可重入)。clear()(:124-137)把这种拒绝当作「teardown 坏了」记 warn 并继续,但真正原因是重入。
另外 ctx.on('session/event', … event.type === 'sandbox/mode' → reconcile())(:213)也踩同一个坑:sandbox/mode 这一条事件本身就在 append 里。
修复建议
- 把 reconcile 挪出追加窗口:
agent/created/session/event触发时不要同步跑,改成queueMicrotask或setTimeout(0),并把同一 tick 的多次触发合并成一次。 attach()只发布新增的 entry,不要顺手全表重建(这是 [BUG] 0.4.1:session-shells 把每次 sandbox/mode 变化当成全表重建信号,新建会话要为每个存活 agent 重新注册一遍 ops_bash #382 的方向)。- 定义未变就短路:把新 definition 与
entry.definition做结构比较,相同则跳过 dispose + register([BUG] 0.4.1:session-shells 把每次 sandbox/mode 变化当成全表重建信号,新建会话要为每个存活 agent 重新注册一遍 ops_bash #382 的评论里也提到这条)。 - 重入类错误不要同步重试:识别出
cannot reenter后把该 entry 标记为待恢复,安排一次延后 reconcile,而不是在同一次调用里再tools.register一遍。 - 顺手一条:
clear()的 catch 把「重入」与「dispose 真的坏了」分开,前者不该按 warn 打三行。
前三条能同时压掉 #382 说的 O(N) 成本;第 1 条才是让 register 不再被拒的那一条。
复现步骤(干净机器)
- 装
dsh-ops@0.4.1,profile 用desktop; - 打开 8–10 个会话,每个都发一句消息把 agent 建起来;
- 再点一次「新建会话」,看
~/.dsh/logs/desktop-next-<date>.0.jsonl:warn 会成片出现,条数 ≈ 3 × 已注册 bash 的存活 agent 数; - 用上面的
createdAt与 mtime 差值量耗时;重启 DSH 后重复第 3 步,条数与耗时同步回落(我这里 39 行 / 2 041 ms → 6 行 / 381 ms)。
附:临时 workaround
目前只能让线性项归零:重启 DSH,或者及时关掉不用的会话(每个关掉的会话都会少一个 ops_bash entry)。
数据采集脚本与原始日志(desktop-next-2026-10-24.0.jsonl、workspace-directory.log)我这边都留着,需要贴更多行可以直接给。
Activity
Haoming9527 commentedon Oct 24, 2026
Claiming the append-window publication fix. I will add a regression that refuses registry writes during session event publication, then defer and coalesce Bash reconciliation while preserving live execution permission checks. This is separate from the incremental cost fix in #383.