Skip to content

[BUG] 0.4.1:新建会话时 reconcile() 在会话追加窗口内重注册 ops_bash,register 全部被宿主拒绝(session append cannot reenter),新会话拿不到 ops_bash #386

Open

Description

@milu7

摘要

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

两个后果:

  1. 新建会话当下拿不到 ops_bash:publish() 的 fix: 修复 Linux 上原生 worker 完全不可用,并补上四条关停路径 (#26) #30 兜底(用 entry.definition 再注册一次)同样被拒,新建那条 entry 的 definition 还是 undefined,于是直接 return,要等下一次 reconcile() 才补上。
  2. 点「新建会话」的延迟随存活 agent 数线性增长:每个已注册过 bash 的存活 agent 会留下 3 行 warn(teardown / publication / restoration),也就是 2 次白烧的追加;实测每行 ≈ 47 ms。

与 #382、#333 是同一触发点的不同失败模式:#382/#333 讨论的是「全表重建的 O(N) 成本」(这些重建在你们机器上大概成功了),这里报的是「重建整批失败 + 重试仍失败 + 新会话因此缺工具」。即使按 #382 的建议把它改成增量,只要 register 仍在追加窗口内同步调用,新建会话自己那一次发布还是会撞上同一个拒绝。

dsh-ops 版本与安装通道

  • dsh-ops 0.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.bundles 12 项(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 里。

修复建议

  1. 把 reconcile 挪出追加窗口:agent/created / session/event 触发时不要同步跑,改成 queueMicrotask 或 setTimeout(0),并把同一 tick 的多次触发合并成一次。
  2. attach() 只发布新增的 entry,不要顺手全表重建(这是 [BUG] 0.4.1:session-shells 把每次 sandbox/mode 变化当成全表重建信号,新建会话要为每个存活 agent 重新注册一遍 ops_bash #382 的方向)。
  3. 定义未变就短路:把新 definition 与 entry.definition 做结构比较,相同则跳过 dispose + register([BUG] 0.4.1:session-shells 把每次 sandbox/mode 变化当成全表重建信号,新建会话要为每个存活 agent 重新注册一遍 ops_bash #382 的评论里也提到这条)。
  4. 重入类错误不要同步重试:识别出 cannot reenter 后把该 entry 标记为待恢复,安排一次延后 reconcile,而不是在同一次调用里再 tools.register 一遍。
  5. 顺手一条:clear() 的 catch 把「重入」与「dispose 真的坏了」分开,前者不该按 warn 打三行。

前三条能同时压掉 #382 说的 O(N) 成本;第 1 条才是让 register 不再被拒的那一条。

复现步骤(干净机器)

  1. 装 dsh-ops@0.4.1,profile 用 desktop;
  2. 打开 8–10 个会话,每个都发一句消息把 agent 建起来;
  3. 再点一次「新建会话」,看 ~/.dsh/logs/desktop-next-<date>.0.jsonl:warn 会成片出现,条数 ≈ 3 × 已注册 bash 的存活 agent 数;
  4. 用上面的 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

  1. Haoming9527 commented on Oct 24, 2026

    @Haoming9527
    Collaborator

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions