Skip to content

fix(listener): prevent daemon stalls during group polling - #1001

Open
Littlezjh wants to merge 2 commits into
deepcoldy:masterfrom
Littlezjh:botmux-listener-local-stall-zjh
Open

fix(listener): prevent daemon stalls during group polling#1001
Littlezjh wants to merge 2 commits into
deepcoldy:masterfrom
Littlezjh:botmux-listener-local-stall-zjh

Conversation

@Littlezjh

Copy link
Copy Markdown
Contributor

Keep restored persistent sessions lazy by default, make dashboard bulk rows lightweight, and normalize/cancel REST chat-history polling so group listeners stay responsive on hosts with large session history.

@Littlezjh
Littlezjh requested a review from deepcoldy as a code owner August 25, 2026 08:52
@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个 PR!poll 超时那部分是很实在的修复——当前 master 的轮询只有 listenerPollInFlight 一把锁、没有超时,某次拉历史 hang 住就会永久卡死监听,这个方向很对。

不过在本地把这个分支对齐到当前 master 后,发现有几处需要先处理,记录在下面供参考(以下是自动化评审的初步意见,最终以维护者审阅为准):

1. 分支需要 rebase(比较关键)
这个分支基于约 3 周前的 master,落后接近 295 个 commit。GitHub 上 diff 看着干净,是因为它对比的是旧基线;rebase 到当前 master 后有 5 个文件产生语义冲突(dashboard-ipc-server / dashboard-rows / session-manager / event-dispatcher / test/session-manager-auto-recover),都是"双方改了同一个函数"。冲突可以解(本地解完 tsc 通过、相关测试全绿),但解的时候有几个坑要踩对:

  • composeRowFromActiveopenTodos:master 在这 3 周里新增了 openTodos: sessionOpenTodos(...),它和 tokenUsage 一样贵(statSync + 冷读整份 JSONL)。这个分支没见过它。rebase 时如果照搬本分支的函数体,会把 master 的 openTodos 静默删掉;如果保留但没放进 lightweight 开关,轻量快照仍会逐行扫 transcript,优化在 active-row 路径上失效。建议把 openTodos 也一起纳入 !opts.lightweight 分支。另外现有的 lightweight 测试只断言了 tokenUsage/previewUserText/previewBotText,没覆盖 openTodos,建议补一条。

  • restoreActiveSessions 的 staggered re-fork 回调:master 现在这里带了两块关键逻辑——对 pending queuedActivation 的精确续跑(recoverExactNonCodex,携带 turnId/dispatchAttempt/resume),以及第 5 个参数的存活性判定(跳过在 staggered 延迟期间被关闭/替换的会话)。本分支的 scheduleStaggeredRecoveryFork(toReattach, (ds) => forkWorker(ds, '', true)) 会把这两块都抹掉,可能导致重启时把排队中的回合丢掉。建议保留 master 的回调与判定,只做"非阻塞化"改造。

  • 另外 restoreActiveSessions 在 master 上多了第 2 个参数 quarantinedSessionIds,以及非持久后端的 hasProtectedSessionMutationOwnership 分支,这些也需要在 rebase 时一并保留(和本 PR 新增的 skippedReattachByBackend 计数并存)。

2. 恢复默认关闭(recoveryForkEnabled 默认 false)会打破既有恢复行为
shouldAutoForkOnRestore 改成默认关闭后,本地实测会打破 test/restore-zombie-close.test.ts 里 9 个既有测试(干净 master 是 50/50 全绿;仅应用这一处改动后变成 42/50)——因为这些测试断言的正是"存活的持久后端会话在重启后自动 re-attach、而不是被误关"。而 stated problem(restore 把消息监听饿死)其实只需要把 re-fork 改成非阻塞就能解决,不必默认关闭。建议二选一:

  • (a) 保留 eager re-fork 默认开启(维持 master 行为),只做非阻塞化 —— 本地验证这样能同时修复卡死问题且不破坏 restore-zombie-close;或
  • (b) 如坚持默认关闭,请同步更新所有被打破的既有测试,并在 PR 描述里说明为何默认关闭是可接受的(重启后持久会话不再自动回来、僵尸 pane 不再自愈、transcript fallback 不再触发)。

3. 有一部分改动 master 已经有了
event-dispatcher 里的 open_bot_id/isOpenIdDomain 发送者身份解析,当前 master 已经实现(且注释更全),rebase 时会自然合并掉。body.content 兜底和 poll 超时是本 PR 真正新增的价值,保留即可。

再次感谢!按上面 rebase + 处理好冲突后应该就能顺畅集成。以上为初步评审意见,具体请以维护者最终审阅为准。

Littlezjh and others added 2 commits August 26, 2026 15:19
Keep restored persistent sessions lazy by default, make dashboard bulk rows lightweight, and normalize/cancel REST chat-history polling so group listeners stay responsive on hosts with large session history.

Co-authored-by: TRAE CLI <noreply@bytedance.com>
@Littlezjh
Littlezjh force-pushed the botmux-listener-local-stall-zjh branch from e3ed777 to f65839a Compare August 26, 2026 07:31
@deepcoldy

Copy link
Copy Markdown
Owner

感谢更新!rebase 做得很干净,之前提到的几处也都处理到位了,先确认一下:

  • ✅ 已 rebase 到当前 master(0 落后),之前的 5 处冲突不再存在;
  • composeRowFromActiveopenTodos 已纳入 !opts.lightweight 分支;
  • restoreActiveSessions 的 staggered re-fork 回调保留了 recoverExactNonCodex 和存活性判定(scheduleStaggeredRecoveryFork 新增 stillOwned 参数转发);
  • quarantinedSessionIds 第 2 参 + hasProtectedSessionMutationOwnership 分支都在。

以下是本轮复审(自动评审的初步意见,最终以维护者审阅为准):

1.(比较关键)有 10 个既有测试被打破
本地实测,这两个文件在干净 master 上全绿、切到本 PR 的 head 后失败:

  • test/restore-zombie-close.test.ts:50/50 → 41/50(9 失败)。根因是保留了"恢复默认关闭"(shouldAutoForkOnRestore 默认 recoveryForkEnabled === true,即默认 false),但没同步更新那些断言"存活的持久后端会话在重启后应自动 re-attach"的用例("exists" → auto-forks to re-attach、ZMX 分类、Herdr managed-agent、Codex App sidecar re-attach、collision 等),外加一处 announceSessionRow 的单参 toHaveBeenCalledWith 被新增第 2 参打破。
  • test/close-consumer-matrix.test.ts:14/14 → 13/14。retention cleanup 在 daemon.ts::run 里新增了一个 closeSessionHelper(...) 调用点,而这个测试要求每个 close 调用点都在 CONSUMERS 里归类。建议把它按 background 类补进 CONSUMERS(或改用 closeSessionForBackgroundCleanup,它会同时记日志)。

看起来这轮只跑了本 PR 直接改动的测试文件(它们都是绿的),没跑到这两个受影响面。对第一个文件建议二选一:

  • (a) 去掉"默认关闭",保留 eager re-fork 默认开启、只做非阻塞化 —— 本地验证这样能同时修复卡死问题且不破坏 restore-zombie-close(推荐);或
  • (b) 如坚持默认关闭,请把被打破的既有测试一并更新,并在 PR 描述说明为何默认关闭可接受(重启后持久会话不再自动回来、僵尸 pane 不再自愈、transcript fallback 不再触发)。

2. 新增的 retention cleanup(第 2 个 commit)有几点想确认
这是个合理的相关方向,但它带破坏性动作(到期 close + 销毁 backing),有几处建议:

  • 默认开启 + 破坏性:DEFAULT_LISTENER.cleanup.enabled = true,cloneListenernormalizeMessageListenerCleanupConfig 也都是"非显式 false 即 true",三层默认开;而 PR 描述里没有提到 retention cleanup。破坏性操作默认开启建议慎重——要么默认关闭 / opt-in,要么在描述和 UI 里显著说明。
  • 启动顺序:await messageListenerCleanup.run('startup') 放在了 await restoreActiveSessions 之前且是 await 的。补充一点:启动时 activeSessions 还是空的,所以这轮走的是"同步 destroyUnregisteredPersistentBacking + 一次性 closeSessionsMatching"(串行 await closeSessionHelper 只发生在后台 interval,不挡 restore),工作量有界。但既然本 PR 的核心就是把活挪出 restore 关键路径,这里又塞了个 await 进去,方向上有点自相矛盾——建议改成非阻塞或挪到 restore 之后。
  • isMessageListenerSession 的判定:优先用结构化的 session.messageListener 字段(只对新话题设置),兜底用 session 文本里包含 <message_listener> 子串。存量监听会话只能靠子串;而在一个既是监听、又有普通用户话题的群里,若某个用户话题的正文恰好包含该字面串,就可能被误判并在到期后关闭。结合 cleanup 默认开启,爆炸半径被放大了。建议收紧到结构化字段(或至少 startsWith / 只查特定前缀)。

另外可以考虑把 retention cleanup 拆成单独 PR,让"修复卡死"这条主线更聚焦。

再次感谢!主要是第 1 点(10 个破测)需要先处理。以上为初步评审意见,具体以维护者最终审阅为准。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants