fix(lark): 修复仓库选择卡吞卡、恢复与搜索 - #756
Open
47seek wants to merge 14 commits into
Open
Conversation
47seek
force-pushed
the
fix/repo-picker-card-overflow-recovery
branch
from
August 6, 2026 14:39
8f0fbdb to
3191bd3
Compare
Collaborator
Author
|
已追加首次
|
将 fix/repo-picker-card-overflow-recovery 与最新 upstream/master 合并,解决 5 处冲突 (command-handler / types / daemon / card-handler / 测试),并对齐本分支「异步仓库扫描」 与 master 新增「durable queued-activation-tail」两套并发缓冲模型: - daemon:仓库选择卡走异步子进程扫描(scanMultipleProjectsAsync + repoScanInFlight), 扫描期间同锚点消息缓冲进 pendingFollowUps 并静默 hold;扫描结束后落 repoCardMessageId 时补 persistPendingRepoCardMessageId 保持重启可恢复。 - handleThreadReplyAdmitted:pendingRepo 预 fork 缓冲按开局形态分流——bare /repo 占位 提升为 durable 开局、raw-root 开局按 deepcoldy#597 暂存 durable 队列尾;普通 prompt 选仓卡的 后续消息仍折进 pendingFollowUps(选仓提交路径消费的正是该 buffer,避免提交时丢消息)。 - card-handler:worktree_toggle_mode 保留跨 await 卡片竞态守卫(isActiveRepoCard / isCurrentRepoCardReplacement),并在切换运行态卡身份前补 persistPendingRepoCardMessageId。 - 仓库搜索结果沿用全量原始 1-based 编号(displayIndexByPath),卡面编号与 /repo N 指向一致。 验证:tsc --noEmit 通过;pnpm build 通过;受影响的定向单测全绿(daemon-repo-scan-routing 22、daemon-rename-route 56、group-join-shared-routing 18、card-handler-repo-select 75、 daemon-codex-app-workflow-wiring 3);三方合并对 master 零冲突。全量单测除依赖 bwrap / tmux 的环境型集成用例(在 master 上同样失败)外全部通过。
异步扫描把 140 仓库扫描挪进 fork 子进程后,runScan 仅在子进程 message/error/close 时结算 Promise。若子进程加载后卡在同步文件系统调用 (挂死的 NFS 网络盘、超大目录树——正是本子系统要隔离的慢扫描场景), 则永不发消息、永不退出 → Promise 永不结算。又因扫描走全局串行队列 (scanQueue),一个卡死子进程会永久堵住此后所有会话的仓库扫描,且无看门狗自愈。 修复: - runScan 加超时(默认 60s,可用 BOTMUX_REPO_SCAN_TIMEOUT_MS 覆盖):超时先 SIGTERM, 2s 宽限后仍在则 SIGKILL。kill 必然触发 close 事件,从而结算 Promise 并让串行队列 继续往下推;超时会走进已有的 catch 分支(撤进度卡 + 发 /repo 文本恢复)。 - 补回归测试:用一个「加载后挂住、IPC 永不回复」的子进程 + 极短超时,断言扫描以 timeout 报错拒绝,且**后续扫描仍成功**(队列未被毒化)。 - childEntryPoint 增加 BOTMUX_REPO_SCANNER_CHILD 覆盖 seam(仅测试/排障用), 以便无需真卡死即可覆盖超时路径。 验证:tsc --noEmit 通过;pnpm build 通过;project-scanner-async 4 测试全绿 (含新超时+队列排空回归,485ms 内完成,无子进程泄漏);daemon-repo-scan-routing / project-scanner / daemon-rename-route 定向套件全绿。
复审在异步扫描子系统上发现三个 blocking,逐个修复: 1. 默认超时会误杀生产扫描:参考部署扫 134 仓库需 ~76s,原 60s 会在扫完前 就杀掉,核心场景永久只剩文本兜底。默认抬到 180s(~2.4x 余量), BOTMUX_REPO_SCAN_TIMEOUT_MS 仍可覆盖。 2. watchdog 依赖 close 事件才结算,不保证解锁队列:不可中断 I/O(挂死 NFS)下 即便 SIGKILL 也可能长期 pending、close 永不到达,全局串行 scanQueue 仍被 永久堵死。改为父进程 OWN 结算——settle() 只结算一次并摘除全部 child listener, 超时时排好 SIGKILL 兜底后**立即** settle(reject),不再等 close;后到(或永不 到达)的 close 不会二次结算或泄漏。补回归测试:子进程 trap SIGTERM、close 不 在宽限内到达,扫描仍在 ~超时点 reject 且队列继续排空。 3. 失败恢复会把同步扫描搬回 daemon 事件循环,重造原始全局卡死(最隐蔽): 兜底文案引导用户 `/repo <项目名>` / 直接 `/repo`,而 resolveRepoSelection 的 bare-name 递归扫描与 bare `/repo` 建卡此前都走同步 scanMultipleProjects()、 直接跑在 daemon 事件循环上——无隔离无 watchdog,恢复指引反而把用户引回本 PR 要消灭的那个卡死。改为 resolveRepoSelection 异步化 + 两处递归扫描(bare-name 解析、bare /repo 建卡)都改走隔离的 scanMultipleProjectsAsync。补回归测试: 嵌套 name 解析命中的同时断言同步 scanner 从未被调用(证明走了异步隔离路径)。 验证:tsc --noEmit 通过;pnpm build 通过;project-scanner-async 5 / repo-selection 10 / command-handler 242 / daemon-repo-scan-routing 22 等定向套件全绿(470 项);全量单测 除依赖 bwrap/tmux 的环境型集成用例(在 master 上同样失败)外全部通过。
上一版 settle() 在结算 Promise 时 clearTimeout(killTimer),把超时分支刚排好的 SIGKILL 升级定时器一起取消了。对忽略 SIGTERM 的卡死子进程(正是要防的那类), SIGTERM 无效、SIGKILL 又永不触发 → 子进程永久泄漏,长跑 daemon 在慢盘上会 不断堆积僵尸扫描进程。 修复: - settle() 只清 timeoutTimer,绝不清 killTimer——结算只负责解锁串行队列,进程回收 由 SIGKILL 升级定时器独立跑完;仅在子进程自身 'close' 到达时才清 killTimer。 - 回归测试补齐「进程最终退出」断言:stubborn child 写出自己的 PID,测试在 grace 过后 poll 该 PID,断言已 dead(SIGKILL 生效、无孤儿泄漏),而不再只断言 Promise 快 reject + 队列排空。已验证测试有牙:重新放回「settle 清 killTimer」的 bug 时, 该测试会在进程存活断言处失败。 验证:tsc --noEmit 通过;project-scanner-async 5 测试全绿(含新回收断言,无孤儿残留)。
上一版只把递归 basename 扫描换成了异步隔离,但 resolveRepoSelection 的直接候选 fast-path 仍在 daemon 事件循环上同步跑 statSync + describeProjectDir——后者会 shell 出多次 git(含 `git describe --tags`,在超大 tag 仓库上 2-6s/次的热点)。挂死 mount 上单次不可中断的 statSync 就能在 watchdog 子进程起来前锁死整个事件循环,隔离不完整。 修复: - 把完整的 `/repo <name|path>` 解析(候选 stat + describeProjectDir 的 git 调用 + 递归 basename 扫描)整体下沉到隔离子进程:新增子进程 resolve 请求类型, project-scanner-async 暴露 resolveRepoSelectionAsync,command-handler 的 resolveRepoSelection 只做薄委托。runChild 复用同一套超时/SIGTERM→SIGKILL/父进程 结算机制,resolve 与 scan 共享全局串行队列与看门狗。 - 无论命中哪条候选路径或回退,都不再有同步 fs/git 落在 daemon loop 上。 - 回归测试:新增「直接候选 stat + git describe 也不在父进程跑」用例(spy 断言 同步 scanMultipleProjects 与 describeProjectDir 均未被父进程调用),与既有 「嵌套 name 递归扫描走异步隔离」用例配套。command-handler / daemon-rename-route 的 /repo 测试 mock 相应对齐(resolveRepoSelectionAsync 走 mock 或原实现)。 验证:tsc --noEmit 通过;pnpm build 通过;command-handler 242 / repo-selection 11 / project-scanner-async 5 / daemon-rename-route 56 / 相关 daemon 套件全绿(468 项); 全量单测仅剩依赖 bwrap/tmux 的环境型集成用例失败(纯 master 上同样失败)。
# Conflicts: # src/daemon.ts
Owner
与 #797 的合并协调说明#797(给同步扫描加进程内预算护栏 本 PR(#756)是同一问题的根治向修法(把扫描 fork 进子进程 + watchdog 超时),层次更深、体量更大,继续按自身节奏收敛即可。 需要注意的一处冲突:#797 合入后,本 PR 会在 建议的解法:保留本 PR 的异步子进程扫描,同时把 #797 的 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
真实环境扫描 132–134 个项目时,首次仓库选择卡会间歇性发送失败,表现为
socket hang up/write EPIPE;更严重的是同步扫描期间第一次@机器人约 1 分钟没有任何可见反馈,容易被误认为消息被吞。现场剖析一次 134 项扫描:
stat/readdir仅约 0.2 秒;git describe --tags --exact-match HEAD,仓库约有 117,370 个 tag,单次可耗 2–6 秒;改动
首次
@不再等待扫描/repo、/close可正常处理。/repo、关闭或其它入口消费,丢弃迟到结果并回收迟到卡,不会二次 fork。/repo文本恢复入口。卡片超限、陈旧连接与失败恢复
repo-picker:<sessionId>provider UUID。EPIPE、ECONNRESET、ECONNABORTED、ETIMEDOUT、socket hang up等瞬时传输错误自动重发一次,首发与重发共用 UUID,避免重复卡。/repo <name|path>文本兜底;route 已消费或正在 commit 时不再发过时恢复文案。lastRepoScan按项目名、分支或路径搜索;搜索结果保持原始完整列表编号。行为说明
@静默。/repo <项目名|路径>,或/repo使用当前工作目录;首轮消息和扫描期间补充均不会丢。验证
/repo接管、empty scan 单次 forkpnpm exec tsc --noEmitpnpm build4ac0b8dd7335git diff --check现场验证
部署
3191bd3a后,在新话题第一次@机器人:/repo;@才唤醒首轮消息。扫描卡交接补充(e2caa193)
验证:聚焦回归 8 files / 513 tests passed;TypeScript、build、diff check 通过。全量 819 files 中 815 passed、3 skipped,1 个无关 worker 状态集成测试在并行负载下超时;该文件随后隔离重跑 3/3 passed。runtime build id: 170f86b9ddaf。