Skip to content

fix(worker): 修复 resume 后无输出时屏幕状态永久停在 working - #1585

Open
xu4wang wants to merge 1 commit into
deepcoldy:masterfrom
xu4wang:fix/resume-first-prompt-idle
Open

xu4wang wants to merge 1 commit into
deepcoldy:masterfrom
xu4wang:fix/resume-first-prompt-idle

Conversation

@xu4wang

@xu4wang xu4wang commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

问题

挂起的会话被网页终端冷唤醒(terminal accessed with no live worker — backing pane is gone, waking cold,没有要投递的消息)后,偶发屏幕状态永久停在 working:

  • claude --resume 在 SessionStart 信号送达 worker 之前就画完了转写和 ❯,之后再无任何 PTY 输出(日志:Ready-gate settle done (quiet 1001ms),此后再无 Prompt detected (idle))
  • SessionStart 边界调 resetReadyEvidence() 清掉 readySeen;readyPattern=/❯/ 时 IdleDetector 的静默策略在 !readySeen 下直接 return
  • 接受「屏幕上已有 ❯」的 post-hook 兜底只对 source=startup arm(input-gate.ts 注释:resume 一定会重绘 ❯——这个前提在这里不成立)
  • 15s 首轮超时对 type-ahead CLI 只调 flushPending(),空队列下是 no-op,markPromptReady() 从不被调用
  • projectRuntimeScreenStatus 在 promptReady=false 时报 working;采样器只在变化时发送 ⇒ daemon 的 lastScreenStatus 恒为 working
  • 后果之一:botmux suspend 在 dashboard-ipc-server.ts 只挂 pendingSuspendReason 返回 deferred,因为永远等不到 idle 边沿而永不兑现

生产上观察到两例(worker 存活数天,tmux 画面停在空输入框),历史约 100 次终端冷唤醒中其余都在 ~1s 内判出空闲。

修复

首轮超时的 type-ahead flush 分支里,当 边界仍未拿到证据、队列为空、边界后网页终端无输入 时,arm 一个兜底轮询(复用 decidePostHookPromptEvidence 的静默/重试/期限语义与 post-hook 兜底的定时器):

  • 只接受 自 arm 以来完全冻住 的画面:PTY 静默满 2s,且最后一个 ❯ 落在上下都有横线的输入框里(screenShowsFramedPrompt,排除启动/信任/hook 选择器的 ❯ 1. Yes)
  • 满足后调 idleDetector.seedReadyEvidence(),仍走完整静默判定(含 spinner guard)→ markPromptReadyFromPty
  • 任何真实 PTY 输出、网页终端输入(新增 webTerminalInputGeneration,在转发给 backend 前登记)、currentBotmuxTurnId 变化、排队或在途输入(新增 InflightInputTracker.hasUnacked())、backend 换代都让兜底退出,交还正常路径——网页终端里直接提交的 turn 不经过队列,不能让它中途的安静间隙被当成空闲
  • 屏幕 resync(观察端重连)不算活动:它 reset 了 IdleDetector 却不喂数据,停掉兜底会再次困住;它只重置静默窗口与等待期限

改动不影响 startup 路径、非 type-ahead CLI、有排队消息的唤醒。

验证

  • test/resume-first-prompt-seed.test.ts(35 例):IdleDetector 层面复现成因(resume 边界后无输出 60s 仍不判空闲)+ 修复机制;shouldArmFirstPromptTimeoutPromptSeed / firstPromptSeedStillWaiting / screenShowsFramedPrompt 行为测试;worker 接线 source-lock
  • 变异验证:对实现中 30 处条件逐一改坏(删 arm 调用、删每个围栏字段、框线只查一侧、取第一个 ❯、框线长度放宽、resync 计入输出、期限不随 resync 重置、边界不登记输入代数……),30/30 使对应测试变红
  • tsc --noEmit 通过;first-prompt-timeout-repro / input-gate 既有测试全绿
  • 全量 vitest --project unit 与未改动的 master 对照:只在改动版出现的失败用例所在的 5 个文件,两边各单独跑 3 遍均全绿(500/500),判为满载 flaky

已知限制

  • worker 的定时器行为本身没有行为级测试(仓库无 worker 级 harness),由纯函数行为测试 + source-lock 覆盖
  • 兜底 arm 后若先来一段不含 ❯ 的真实输出、随后永久静默,兜底会退出,仍停在 working(与修复前相同,未变差)

🤖 Generated with Claude Code

挂起的会话被网页终端冷唤醒(没有要投递的消息)时,claude --resume 可能在
SessionStart 信号送达 worker 之前就画完了转写和 ❯,之后再无输出。边界把 ready
证据清零后,IdleDetector 的静默策略被 `!readySeen` 永久压住;type-ahead CLI 的
首轮超时只调 flushPending(),空队列下什么也不做。isPromptReady 于是永远为
false,屏幕状态恒报 working,等 idle 边沿的延迟挂起(dashboard suspend 返回
deferred)永远不兑现。

首轮超时时,若边界仍未拿到证据、队列为空、边界后网页终端无输入,则启动一个
兜底:只接受自 arm 以来完全冻住的画面——PTY 静默满 2s,且最后一个 ❯ 落在上下
都有横线的输入框里(排除启动/信任对话框里的 `❯ 1. Yes`)——然后补一次 ready
证据,交给既有的静默判定 → markPromptReady 链路。任何真实 PTY 输出、网页终端
输入、turn 变化、排队或在途输入、backend 换代都会让兜底退出,交还正常路径;
屏幕 resync 只重置静默窗口与等待期限。兜底复用 post-hook 兜底的定时器,spawn/
kill 的清理路径一并覆盖。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@deepcoldy

Copy link
Copy Markdown
Owner

你好 @xu4wang,我们已为这个 PR 建了飞书评审群,首次 review 正在进行中。自动邀请你入群时因应用可见范围/外部协作权限限制没能直接拉入,麻烦点击下面的一年有效链接自行加群,后续评审意见和修改沟通都会在群里同步:

https://applink.larkoffice.com/client/chat/chatter/add_by_link?link_token=d95vd84e-4023-46ef-947b-091e429dd189

也可以把你的 GitHub 账号和飞书信息补进这份名单文档,之后的 PR 评审群就能自动拉你:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe

(本条为自动流程发出,最终评审结论以维护者审阅为准。)

This branch has not been deployed

No deployments
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