Skip to content

feat(desktop): 更新重启只在有任务运行时才二次确认,并改成中断警告 - #1197

Merged
MagicLizi merged 11 commits into
mainfrom
dash/update-relaunch-single-confirm
Jul 31, 2026
Merged

feat(desktop): 更新重启只在有任务运行时才二次确认,并改成中断警告#1197
MagicLizi merged 11 commits into
mainfrom
dash/update-relaunch-single-confirm

Conversation

@dashhuang

@dashhuang dashhuang commented Jul 31, 2026

Copy link
Copy Markdown
Member

这次改了什么

摘要

应用更新的重启入口原来要点两次:点侧栏 banner 的「立即重启」→ banner 就地切成确认态 →
再点「确认重启」才真重启。问题是没有任何任务在跑时,第二步只写着「应用会自动重启」 ——
不带任何信息量,纯粹多要一次点击。

现在点入口先查 anySessionInTurn()

  • 确认没有任务在跑 → 直接重启,一次点击到底;
  • 有任务在跑,或探针拿不到可信答案 → 才拦一次,并且那一屏改成中断警告:标题点明状态
    (「仍有任务在运行中」)、副标题讲后果(警告色,「重启会中断运行中的任务」)、主按钮
    「仍要重启应用」,取消仍在下方次级位。

探针失败刻意 fail closed:重启会不可撤销地杀掉 in-flight turn,「无法确认」不能当成
「确认没有」。这条口径跟的是 main 侧托盘退出路径 —— bootstrap-electron.ts
hasActiveTurn 对同一个探针 catch → true,注释写明「A failed busy probe must not turn
the tray into an unguarded exit path.」。(renderer 侧 WindowControls.handleCloseClick
catch → false 是既有行为,本 PR 不动它;要统一应作为同时覆盖关窗与重启的独立改动。)

变更类型

  • feat 新功能
  • fix 缺陷修复
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:

范围

  • 关联 Issue / 需求:无(产品体验诉求:更新不该无条件确认两次)
  • 本 PR 包含:
    • UpdateBanner 重启入口改为「先查 busy 探针,再决定直接重启 vs 进中断警告态」,展开态
      与收起 / rail 态共用同一判定;
    • 入口点击加 ref 防重入(探针在飞时的连点不会重复探针、也不会重复重启);
    • confirming 态语义收窄为「有任务在运行,重启会打断它」,警告色与文案不再有分支;
    • 退役无信息量的 confirmHint,四语言(zh-CN / en / ja / ko)同步改写确认态文案;
    • 测试随语义更名并重写,另修一条依赖旧行为的既有用例。
  • 明确不包含:
    • 不触碰 apps/desktop/src/main/updateService.tsapps/desktop/cindy-updater/(Tauri)
      或重启命令本身 —— 纯 renderer 交互层 + i18n 文案;
    • 不改 anySessionInTurn() 的 busy 判定口径(沿用 sessionTurnActivityTracker 既有语义);
    • 不改自动更新的下载 / 校验 / superseding 链路,也不改装完之后的公告弹窗。
  • 用户可见变化:
    • 无任务在跑时,更新重启从「两次点击」变成「一次点击」;
    • 有任务在跑时,第二步从泛化的「确认重启更新?/ 应用会自动重启」变成明确的
      「仍有任务在运行中 / 重启会中断运行中的任务 / 仍要重启」。
  • 是否存在 breaking change:无

UI 变化

不涉及新增视觉元素与样式:布局、图标、按钮形态、间距、动效全部沿用既有 banner 的
ready / confirming 两态结构,本次改的是进入 confirming 的条件其中的文案,以及
把原先按 busy 分支的警告色改为常量(confirming 态一定是 busy 才进来的)。

  • 引用的设计规范:
    • DESIGN.md §10「Light / Dark Dual-Mode Delivery Gate」——颜色一律走语义 token:
      警告文案沿用 --warning-fg,按钮沿用 --update-btn-bg / --update-btn-border /
      --update-btn-text / --update-btn-hover,取消沿用 text-sidebar-muted +
      hover:bg-sidebar-item-hover。本次没有新增任何硬编码颜色,也没有只适配单一模式的
      条件补丁。双模式实机目检未完成,原因见「未执行的验证」,按该节「Verification is
      best-effort and must be reported honestly」如实登记,不把「复用了 themed token」
      当成「双模式已验证」。
    • DESIGN.md §11.1「Actions = verb + object, never a bare verb」+ §11.2 英文按钮
      Title Case——主按钮从「确认重启」改为「仍要重启应用」/Restart App Anyway(ja
      「それでもアプリを再起動」、ko「그래도 앱 재시작」),带上动作对象、脱离上下文也读得通,
      没有使用被禁的裸「确定 / 确认 / OK」;副标题按「what happened + what to do」的精神写成
      「状态 + 后果」,让用户能判断该不该现在重启。
    • DESIGN.md §11.3「Self-Check (when touching copy)」——四份 common.json 全部更新,
      按各语言标点惯例书写(zh-CN 无句末句号、ja/ko 沿用各自惯用表达,未自造译法)。
    • i18n/GLOSSARY.md Running 条目——zh-CN 用裁决译法「运行中」。初版写的「进行中」是该
      条目下的条件禁用译法,已被 pnpm check:i18n-glossary 拦下并改正。
    • 两处未跟改的边界:入口按钮(Relaunch / 「立即重启」)与 cancel 是本 PR 未改语义的
      既有 key,动它们会连带 tooltipReady / ariaCollapsed / ariaExpanded 一串既有文案,
      属于「整块 banner 文案对齐规范」的独立改动;confirmTitleA task is still running
      是状态陈述句而非标签,与同 banner 的 Newer version found 同形,保持 sentence case。

UI 证据:截图这条路走不通(dev 被 isDev() 短路、拿不到真实 ready 态),已按 auto-review
提示的另一条路附上自包含 HTML 静态复现 —— 三态 × Light / Dark,见
UI 证据评论
那不是实机渲染:颜色抄自主题 token 文件、布局按 class 等价还原,边界已在评论里标注。
评论里另记了一条静态复现暴露的既有问题(Light 下 --warning-fg 对比度约 1.8:1,非本 PR
引入、未被本 PR 加重,按 §10 留给设计裁决,本 PR 不自行改色)。

不变量清单(给 reviewer 的锚点)

入口判定只有两条不变量,全部 UI 分支与两个入口(展开态 / 收起 rail 态)都复用同一份实现:

  1. 只有「确认没有任务」才允许直接执行。 判定在 handleRelaunchClick 一处完成,
    但「有任务」有两个独立来源,必须都查(并行,互不依赖):

    来源 覆盖什么 漏掉的后果
    anySessionInTurn() 逻辑 turn(SessionTurnActivityTracker + live session 的 isTurnRunning 正在跑的 turn 被无确认打断
    maker.listSessionBackgroundActivity() turn 已结束但 CC 子进程仍在调模型:后台子 agent、run_in_background 的 Bash。channels.ts 把它维护成与 turn 完全独立的状态 父 turn 刚结束、后台任务还在跑时被 forceQuit() 直接打断或留成失联进程

    hasInFlight 初值与 catch 都取 true,所以任何拿不到可信答案的路径(任一查询
    reject、桥同步 throw)都落在保守侧 —— 破坏性入口上「无法确认」不能当成「确认没有」,
    口径同 bootstrap-electron.ts 托盘退出的 hasActiveTurn

    刻意未覆盖(不假装全覆盖):scheduler「run 已启动但 turn 尚未开始」的窄窗口。
    renderer 侧没有该查询入口(main 的无人值守探针另走 SQLite hasRunningRuns),且 run
    真跑起来后会产生 turn、落回第一个来源。

  2. 一次点击的探针结论,只有在这次点击仍然有效时才能驱动副作用。 失效路径全枚举:

    路径 打点位置 不作废的后果
    用户点「稍后再说」 handleDismissrelaunchEpochRef 前进 点了稍后却突然重启;或 confirming 残留,下次被火焰按钮唤回时直接落在第二步
    status 离开 ready(superseding / error / idle) status effect → epoch 前进 continuation 直接读 statusRef 按旧快照重启,装回被顶掉的旧补丁
    组件卸载 卸载 cleanup → epoch 前进 卸载后仍真的把 app 重启掉
    探针在飞时重复点击 relaunchProbeRef 防重入 重复探针 / 重复重启

    status 那一行有两道判定不是冗余:effect 打点会晚一拍,「已 setState 但 effect 未执行」的
    区间里探针恰好 resolve 会读到过期的 ready,所以 continuation 额外直接读最新值。

四条失效路径各有一条测试用例,两个 busy 来源各有一条,且都验证过能区分错误修法 —— 临时
移除对应判定后相关用例全红,恢复后全绿。

已知债:仓内「什么算 busy」有五处不同定义

review 过程中三轮都在同一个主题上(判定收窄后的覆盖面),根因是这个语义在仓里没有单一
事实源。按当前 HEAD 盘一遍,供后续 reviewer 与后续改动参考:

位置 覆盖范围 探针失败
本 PR 的更新重启入口(renderer) turn + 后台活动 fail closed
WindowControls.handleCloseClick(renderer 关窗) turn fail open
quitFromWindowsTrayhasActiveTurn(main 托盘退出) turn fail closed
setUpdateAutoRelaunchBusyProbe(main 无人值守自动重启) turn + 远程 controller + remote invoke + scheduler running runs fail closed
SESSION_IN_TURN(per-session,控制端 stall 看门狗) 单会话 turn

本 PR 不统一它们:关窗链路与 main 侧无人值守探针分别属于既有行为和
docs/dev-rules/cindy-updater.md 门禁管辖范围,合并需要独立 PR 并与维护者确认。这里只
保证新入口自身的覆盖面是完整且保守的。也刻意没有把无人值守那套直接套用到手动入口 ——
它含远程 controller / remote invoke,对无人值守是对的,但用户主动点重启时「有远程设备在看
会话列表」不构成「会被打断的任务」,套过来只会产生误报警告。

判定收敛:三个活动来源合并到 main 侧一处

review 连续三轮指出同一形状的问题:又一个活动来源没被覆盖(逻辑 turn → Claude 后台活动 →
Ghost card-action)。根因不是漏了某个来源,而是判定放在 renderer 侧逐个枚举,而「有哪些活动
来源」在本仓没有单一事实源。已改成 main 侧一处判定(main/relaunchBusyActivity.ts),
renderer 只问一次结论 —— 新增活动来源以后只改那一个函数

活动来源 main 侧事实源 本 PR 覆盖
逻辑 turn SessionTurnActivityTracker + live session isTurnRunning()
Claude 后台活动(后台 subagent,不含后台 Bash) listActiveClaudeBackgroundActivitySessions()
Ghost card-action 后台活动(完全不经 LLM turn) GhostSessionActivityTracker.anySessionBusy()(本 PR 新增)
后台 Bash(run_in_background,不调模型、不折算 running) live session 的 listBackgroundTasks()
Cindy slot 异步代办(mode:'submit' 的图 / 视频生成,脱链执行) GhostCindySlot.anyAsyncJobRunning()(本 PR 新增)
scheduler running run(script 模式 / pre-run hook 不创建 session) readUpdateRelaunchScheduleBusy()(SQLite,异步)
远程 controller / remote invoke getUpdateRelaunchControllers() / hasInFlightRemoteInvokes() ❌ 刻意不纳入

范围边界(给 reviewer 的明确口径)

基线是「无条件重启」。 改动前用户点「立即重启」要点两次,但那次二次确认不做任何活动
判定
—— 文案只有「确认重启更新?/ 应用会自动重启」,全程不提任何正在跑的东西。用户不知道
有任务在途,点下去照样 forceQuit()。所以「旧流程还有第二次确认」不构成保护,把一次没有
信息量的点击算作安全层,是把用户的无知当成机制。

本 PR 的目标是挡住影响最大的那一类,不做穷尽枚举。 已覆盖的六个来源都指向同一类事故:
用户能感知的、正在产出结果的工作被打断(agent turn、后台 subagent、后台 Bash、Ghost
card-action、生成图 / 视频的代办、定时任务)。仓里的异步持有者是开放集合(插件网络请求、
语音、媒体导入、plugin market 下载、device-link 在途 invoke……),继续逐个纳入的边际收益
递减、误报成本递增 —— 而误报也是产品损伤:判定越宽,被拦的比例越高,就越接近本 PR 要
消除的那个「几乎总是要确认两次」。每一条未覆盖来源的基线都是「改动前同样被杀」,因此
不构成回归、不阻塞本 PR

真要做到「重启不打断任何在途工作」,正确形态是让手动重启走优雅退出链、由各活动持有者自己
收尾 —— 那是一个独立的、需要动更新链路的改动,不该由这个交互优化附带。

这份清单不保证完备,但漏掉某个来源不构成回归。 仓里的异步活动持有者是开放集合,每个模块
各自在私有结构里管在途状态,没有统一注册处。关键是:改动前那次二次确认根本不做任何活动
判定
(文案只有「应用会自动重启」),对所有来源都是「点一下就 forceQuit()」。所以覆盖到的
来源是净收益(从无警告被杀变成明确警告),没覆盖到的与改动前行为完全一致。重启路径本身沿用
既有逻辑、一行未改;发现新来源就往 relaunchBusyActivity.ts 加一条。

  • 为什么 Ghost 必须在 main 侧取ghostSessionActivityStore 只靠 ghosts:session-activity
    推送累积集合,preload 里没有 list / 快照 IPC(Claude 后台活动有),而推送只送 0↔1 转变、
    store 又是「首次被消费时才挂订阅」—— banner 挂载时若某会话早已在跑 card-action,renderer
    根本看不到。所以「在 renderer 补第三个查询」这条路做不对。
  • scheduler 已纳入:最初以为「run 跑起来会产生 turn」可以落回第一个来源,但 script 模式与
    pre-run hook 阶段明确不创建 session(script-runner.ts
    'script execution does not support worktrees or bound sessions'),所以必须单独查。判定因此
    变成 async:先读内存源,都空闲才查 SQLite,拿到结果后复采一次内存源关掉查库期间的窗口。
  • 远程 controller 不纳入:那是无人值守自动重启该管的(setUpdateAutoRelaunchBusyProbe
    连「有远程设备在看会话列表」都让路)。手动重启是用户主动发起,远程 viewer 不构成「会被
    打断的任务」,纳进来只会产生误报警告 —— 反而回到本 PR 要消除的那种无信息量确认。
  • fail closed 落在两层:main 侧对每个来源各自 try/catch(抛错记为
    <source>-probe-failed 并算忙),renderer 再兜住整条 IPC 失败。

未触及更新链路updateService.tscindy-updater/ 一行未碰 —— 本 PR 只提供判定查询,
不改下载 / 校验 / 替换 / 重启执行路径,因此不在 docs/dev-rules/cindy-updater.md 的门禁范围内。

远程与手机版适配结论(remote-and-mobile-adaptation.md 门禁三选一)

  1. workdir / agent 进程 / 会话数据:不涉及。新增判定只读内存态跟踪器(turn / 后台活动 /
    Ghost 活动),不碰 workdir 文件,SSH 远程工作区下语义不变。
  2. 新增 IPC channel update-relaunch:blocking-activity不登记 device-link allowlist。
    依据是 packages/device-link/src/allowlist.ts 顶部注释把「updater / release-notes」列为永不
    放行类别;且远程控制端不会代替用户点被控端的更新重启(更新是本机行为)。不进表即天然不可
    远程调用,符合该文件的默认拒绝制。
  3. 手机版入口 / UI:不涉及。手机版是纯控制端,没有「桌面自动更新重启」这个入口。

怎么验证的

自动验证

pnpm test:unit
结果:PASS 56 workspaces / FAIL 0(含 apps/desktop unit 54.2s),exit 0

pnpm --filter desktop run --if-present typecheck
结果:tsc --noEmit 通过(先补 `git submodule update --init --recursive cindy-protocol`,
      新 worktree 未初始化 submodule 会报一批 @cindy/*-protocol 找不到模块)

pnpm check:i18n-glossary
结果:✅ 术语表 27 条已裁决 / 24 条待讨论,en / zh-CN / ja / ko 无新增违规

pnpm check:i18n
结果:✅ zh-CN / en / ja / ko 共 6082 个 key 全部一致

pnpm check:dco
结果:DCO check passed: 1 commit signed off

第二轮(c9100d10,review 反馈修复后)复跑:

pnpm test:unit(走 git skill 的 run-unit-gate.sh 统一入口)
结果:GATE_EXIT=0,PASS 56 workspaces / FAIL 0(含 apps/desktop unit 61.0s)

pnpm --filter desktop run --if-present typecheck
结果:通过

第三轮(fd8dd0aa,fail-closed + 按钮文案)复跑:

pnpm test:unit(run-unit-gate.sh)
结果:GATE_EXIT=0,PASS 56 workspaces / FAIL 0(含 apps/desktop unit 55.8s)

pnpm --filter desktop run --if-present typecheck
结果:通过

pnpm check:i18n-glossary / pnpm check:i18n
结果:均通过(四语言 6082 个 key 一致)

第四轮(b1393863,busy 判定补后台活动)复跑:

pnpm test:unit(run-unit-gate.sh)
结果:GATE_EXIT=0,PASS 56 workspaces / FAIL 0(apps/desktop 57.5s、apps/mobile 9.5s)

pnpm --filter desktop run --if-present typecheck
结果:通过

首跑时 apps/mobilestartupSplashOverlay 有一条用例 5s 超时;单独跑 733ms 通过、
重跑全量亦通过,是负载下的 flake,与本改动无关。

第五轮(f27cdfad,判定收到 main 侧一处 + 覆盖 Ghost)复跑:

pnpm test:unit(run-unit-gate.sh)
结果:GATE_EXIT=0,PASS 56 workspaces / FAIL 0(apps/desktop 60.3s、apps/mobile 11.0s)

pnpm --filter desktop run --if-present typecheck
结果:通过

新增用例:relaunchBusyActivity 9 条(三源各一条 + 多源并发 + 三源各自 fail-closed + 一源
抛错不影响其它源)、ghostSessionActivityanySessionBusy 4 条(含多会话引用计数与 TTL
兜底)。renderer 侧删掉两条职责已搬走的用例,改为只测「拿到 true 就拦、false 才走、拿不到
就保守」这一契约。

针对性用例(apps/desktop/src/renderer/__tests__/updateBannerRelaunchEntry.test.tsx
updateBannerBusyHint.test.tsx 更名重写):

  • 有 turn 在跑 → 出现警告色中断提示,此时不重启;点「仍要重启」才调一次 relaunchToUpdate
  • 没有 turn → 首次点击就重启,且不再出现第二步按钮与提示
  • 只有后台活动、没有 turn → 进入中断警告且不重启;点「仍要重启应用」才重启
  • 探针同步 throw(桥不可用)→ 进入中断警告且不重启(fail closed)
  • turn 探针 reject / 后台活动查询 reject → 同上(两个来源各一条)
  • 探针未 settle 期间连点三次 → 探针只调一次、期间不重启,探针 resolve 后只重启一次
  • 收起 / rail 态入口 → 同一判定(确认无 busy 才直接重启,busy 显示 ✓ / ✕)

另修 updateBannerReleaseNotesLink.test.tsx:其中「确认态隐藏公告文字链」一条依赖「点入口
必然进确认态」,现改为把探针 mock 成 busy 后再断言,用例原意不变;该文件同时补上了
window.electronAPI 桩(入口现在会调 IPC)。

手工验证

不涉及 —— 见下。

未执行的验证

  • 实机未验证:dev 下更新链路被 isDev() 短路(apps/desktop/src/main/updateService.ts
    updateChannelKind() / relaunch 策略),开发实例拿不到真实 ready 态,走不完
    「ready → 点入口 → 探针 → 重启 / 中断警告」的真流程,也无法验证真实重启动作。
  • Light / Dark 双模式:静态复现层面已看,实机仍未验证。上面那份 HTML 复现里两种模式都
    完整渲染并逐态检查过(胶囊反相中性在两侧都可读、警告色可辨、布局无挤压),但它用的是抄过去
    的 token 值而非真实渲染管线,不能替代实机目检;真实 ready 态下的观感仍属未验证。按
    DESIGN.md §10「Verification is best-effort and must be reported honestly」如实登记到这个
    粒度,不把静态复现说成实机已验证。
  • 未跑 pnpm test:all:改动局限于 renderer 单组件 + i18n 文案,按风险选择 test:unit
    作为门禁,其余交给 CI。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:更新链路(docs/dev-rules/cindy-updater.md 管辖范围的交互层)

影响与回滚

  • 影响范围:仅桌面端侧栏 UpdateBanner 的重启入口交互与四语言确认态文案。
    更新器改动已与 owner 确认(按 docs/dev-rules/cindy-updater.md 的核心门禁),
    且本次未触及 updateService.tscindy-updater(Tauri),不改变下载、校验、
    替换、重启命令本身的任何行为,Windows / macOS 的安装—替换—重启路径不受影响。
  • 已知取舍:无任务在跑时少了一道确认,误点入口按钮会立刻重启。入口位于侧栏底部
    banner、需要明确点击,且这与「点窗口 X 直接关掉应用」的既有风险等级一致(关窗同样
    会中断 turn,也同样只在有 in-flight turn 时才确认)。真正需要保护的场景(有任务在跑)
    反而比改动前更明确 —— 从一句中性的「应用会自动重启」变成写明会中断任务。
  • 回滚 / 降级方式:单 commit,git revert 即可完全恢复原「无条件两段式」行为;没有
    持久化状态、没有 schema 与协议变更,回滚无残留。

提交前检查

  • 已 review 完整 diff
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO
  • UI 改动已在「UI 变化」注明引用的设计规范章节(不涉及 UI 则跳过)
  • 未提交凭证、令牌或授权文件
  • 已补充必要文档(无需新增:改动未引入新规则或新概念,组件头注释已同步说明新判定)
  • 已确认测试结果或说明未执行原因

侧栏 UpdateBanner 的重启入口原来是无条件「就地两段式」:点「立即重启」先切
确认态,再点「确认重启」才真重启。没有任何任务在跑时,第二步只有一句「应用会
自动重启」——不带信息量,纯粹多要一次点击。

改成与关窗链路(WindowControls.handleCloseClick)同一套判定:点入口先查
anySessionInTurn(),没有任务在跑就直接重启;只有真的有任务时才拦一次。探针
失败(桥不可用 / handler 未注册)按「没有任务」处理,与关窗链路一致。

因此 confirming 态的语义从「泛化二次确认」收窄为「有任务在运行,重启会打断它」
的中断警告:标题点明状态、副标题讲后果(警告色,不再有分支)、主按钮改为
「仍要重启」。退役无信息量的 confirmHint,四语言同步。

- 入口点击加 ref 防重入:探针在飞时的连点不会重复探针或重复重启
- 收起 / rail 态共用同一判定;那里没有文案位置,中断提示落在 ✓ 的 tooltip 上
- 测试随语义更名为 updateBannerRelaunchEntry.test.tsx,覆盖 busy 拦一次、非
  busy 一击直达、探针抛错、连点防重入、收起态同判定

验证:pnpm test:unit、desktop typecheck、check:i18n-glossary、check:i18n 全绿。
更新链路改动已与 owner 确认;未触及 updateService.ts 与 cindy-updater。
实机:dev 下更新链路被 isDev() 短路,拿不到真实 ready 态,双模式目检未在真实
ready 态下完成;本次未新增样式与颜色,确认态仍复用既有 --warning-fg /
--update-btn-* 语义 token。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
@dashhuang
dashhuang requested a review from a team as a code owner July 31, 2026 08:46
Copilot AI review requested due to automatic review settings July 31, 2026 08:46
@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 将手动更新重启改为按活动状态决定是否显示中断警告。

  • 新增 main 侧统一忙闲判定,聚合 turn、Claude/Ghost 后台活动、后台 Bash、Cindy slot 与 scheduler 活动。
  • 新增受信 renderer 限制的 IPC 查询,并在 preload 暴露给 UpdateBanner。
  • UpdateBanner 在确认空闲时直接重启,在忙碌或探针失败时显示中断警告,并处理重复点击与异步结果失效。
  • 更新四种语言的警告文案,并补充聚合判定、IPC 边界及 renderer 交互测试。

Confidence Score: 4/5

当前仍不宜合并,因为忙闲查询结束到 main 执行重启之间启动的新任务会被无警告终止。

先前线程指出的最终复核缺口在当前 HEAD 仍存在:UpdateBanner 根据 renderer 获取的空闲快照直接发送 update-relaunch,而 main 的执行入口没有在 forceQuit 前重新检查活动状态。

Files Needing Attention: apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx, apps/desktop/src/main/updateService.ts

Important Files Changed

Filename Overview
apps/desktop/src/main/relaunchBusyActivity.ts 新增六类活动来源的统一 fail-closed 聚合判定,并在 SQLite 查询后复采内存来源。
apps/desktop/src/main/relaunchBusyActivityIpc.ts 新增带受信 renderer 校验和幂等注册的忙闲查询 IPC。
apps/desktop/src/main/bootstrap-electron.ts 为手动更新重启装配 Maker、Ghost、Cindy slot、后台 Bash 与 scheduler 活动来源。
apps/desktop/src/main/cindy-brain/cindySlot.ts 为同步、异步及媒体操作增加统一的在途工作快照。
apps/desktop/src/main/cindy-brain/ghostSessionActivity.ts 为 Ghost 活动跟踪器增加跨会话忙碌查询。
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx 重启入口改为先查询忙闲状态,并增加 fail-closed、异步结果失效及点击防重入处理。
apps/desktop/src/preload/preload.ts 向 renderer 暴露手动重启阻断活动查询接口。

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[用户点击立即重启] --> B[renderer 请求 blocking-activity]
  B --> C[main 聚合六类活动来源]
  C -->|忙碌或探针失败| D[显示中断警告]
  D -->|仍要重启| E[发送 update-relaunch]
  D -->|取消| F[返回 ready 状态]
  C -->|确认空闲| E
  E --> G[main 执行更新重启]
Loading

Reviews (11): Last reviewed commit: "fix(desktop): 重启探针 IPC 改为幂等注册,防 splash 重..." | Re-trigger Greptile

Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx Outdated
Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6c2b63c60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx
Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

本 PR 针对 Desktop 侧栏 UpdateBanner 的“更新后重启”入口做交互优化:只有在检测到仍有任务(turn)运行时才进入二次确认,并把该确认态收窄为“中断警告”;无任务时改为一次点击直接重启。同时同步更新四语言文案与相关单测,确保展开态与收起/rail 态入口使用同一判定逻辑。

Changes:

  • UpdateBanner 的重启入口改为点击后先探测 anySessionInTurn():busy 才进入 confirming(中断警告),否则直接 relaunchToUpdate,并增加探针 in-flight 防重入。
  • confirming 态文案语义收窄为“仍有任务在运行中 / 重启会中断运行中的任务 / 仍要重启”,退役无信息量的 confirmHint,四语言同步更新。
  • 新增/重写与更名相关测试用例,覆盖 busy / non-busy / 探针 throw / 连点防重入 / collapsed 入口一致性;并修复一条依赖旧行为的 release-notes link 用例。

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx 重启入口改为 busy 探针后再决定“直接重启 vs 中断警告确认态”,并在探针 in-flight 期间防重入
apps/desktop/src/renderer/i18n/locales/zh-CN/common.json 更新 confirming 态文案为中断警告语义,移除 confirmHint
apps/desktop/src/renderer/i18n/locales/en/common.json 同步英文 confirming 态文案为中断警告语义,移除 confirmHint
apps/desktop/src/renderer/i18n/locales/ja/common.json 同步日文 confirming 态文案为中断警告语义,移除 confirmHint
apps/desktop/src/renderer/i18n/locales/ko/common.json 同步韩文 confirming 态文案为中断警告语义,移除 confirmHint
apps/desktop/src/renderer/tests/updateBannerReleaseNotesLink.test.tsx 适配入口点击新增 busy 探针;修正“confirming 期间隐藏公告链接”的断言方式
apps/desktop/src/renderer/tests/updateBannerRelaunchEntry.test.tsx 新增重启入口判定测试,覆盖 busy/non-busy/throw/连点防重入/收起态一致性
apps/desktop/src/renderer/tests/updateBannerBusyHint.test.tsx 退役旧测试文件(原覆盖 confirmHint/busy 分支),由新语义用例替代

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx
review 指出 handleRelaunchClick 的 await 之后缺少失效保护。归族后是同一条
不变量:**一次点击的探针结论,只有在这次点击仍然有效时才能驱动副作用**。
三条对称路径在探针在飞期间都会让它失效,之前一条都没管:

- 用户点「稍后再说」→ 探针 resolve 后仍会重启(点了稍后却突然重启),或
  setConfirming(true) 残留,下次被火焰按钮唤回时直接落在第二步;
- 已就绪补丁被 superseding 顶掉 → 仍按旧快照重启,装回旧补丁;
- 组件卸载 → 仍会真的把 app 重启掉。

修法是一个点击有效性令牌(relaunchEpochRef):dismiss、卸载、status 离开
ready 都让它前进,在飞的 continuation 靠 epoch 不匹配自我作废。status 变化
额外再读一次 statusRef —— effect 打点会晚一拍,「已 setState 未跑 effect」
那段窗口里探针恰好 resolve 会读到过期的 ready,所以 continuation 直接读最新
值关掉它。两道判定针对同一不变量的不同触发路径,不是防御性冗余。

测试补齐四条对称用例(dismiss、dismiss+busy 结果的状态残留、superseding、
卸载)。每条都验证过能区分错误修法:临时移除两道判定后四条全红,恢复后全绿。
其中「dismiss + busy」初版写成了永远通过的无效断言(dismiss 后 banner 本就
return null),已改成模拟「点 X → 火焰按钮 restore 唤回」来真正暴露残留的
confirming state。

验证:pnpm test:unit(56 workspace PASS / 0 FAIL)、desktop typecheck 通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 09:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9100d10fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx Outdated
Comment thread apps/desktop/src/renderer/i18n/locales/en/common.json Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (2)

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:190

  • 这里的注释仍沿用旧的“确认重启”表述,但 confirming 态现在语义已收窄为“仍要重启”的中断警告(仅 busy 时出现)。建议同步更新注释用词,避免和 UI/状态语义不一致。
  // 直接落在「确认重启」界面(那是两步流程的第二步,越过第一步显示不合适)。
  // 传入当前 status/version 让 store 记录快照,用于后续区分「同一更新 remount」
  // 与「真正新更新到达」,避免导航到 /settings 再回来时误 restore。
  // 同时作废在飞的探针:用户点「稍后再说」就是明确表达「现在不要重启」,几毫秒后 resolve
  // 的探针结论不能反过来推翻它(否则轻则 confirming 残留、重则直接重启)。

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:79

  • 这里注释把“用户又点了一次入口”也列为会让 epoch 前进的失效路径,但当前实现里 relaunchEpochRef 只会在 status 离开 ready、dismiss、卸载时递增;入口重复点击是在 relaunchProbeRef 层面被忽略的。建议把注释改成与实际失效条件一致,避免误导后续维护。
  // 一次点击的有效性令牌。探针是异步的,点击那一刻成立的前提在 resolve 时可能已经不成立:
  // 用户点了右上角「稍后再说」、新版本把已就绪补丁顶成 superseding、组件被卸载,或用户
  // 又点了一次入口。任一情况都让 epoch 前进,在飞的 continuation 靠 epoch 不匹配自我作废。

两条 review 反馈,都采纳:

1. 探针失败不再当作「没有任务」。上一版把 catch 兜底写成 false,论证是「与
   WindowControls.handleCloseClick 同构」——那个论证只看了半个仓库。
   bootstrap-electron.ts 的托盘退出路径对同一个探针是 fail closed 的,注释写得
   很明确:「A failed busy probe must not turn the tray into an unguarded exit
   path.」重启会不可撤销地杀掉 in-flight turn,跟托盘退出同属破坏性入口,所以
   跟的是这条口径,不是关窗那条更宽松的一半。「无法确认」不能当成「确认没有」。
   代价只有探针真失败时多出来的一次确认;正常路径(handler 已注册、main 侧是
   纯同步 snapshot)不受影响。初值也从 false 改成 true,让所有「没拿到可信答案」
   的路径都落在保守那侧,而不是只补 catch 分支。

2. 确认按钮补上动作对象并改 Title Case:Restart anyway → Restart App Anyway,
   仍要重启 → 仍要重启应用,ja/ko 同步。DESIGN.md §11.1 明确要求确认对话的主
   按钮带宾语(好让它脱离上下文也读得通),§11.2 要求英文按钮 Title Case。上一版
   在 PR 描述里把「沿用同 banner 内既有 sentence case」写成刻意偏离,但既有文案
   本身就是欠的债,不构成新写的文案继续欠的理由。入口按钮(Relaunch / 立即重启)
   与 cancel 属既有 key,不在本 PR 范围内动。

测试:探针 throw 的用例从「直接重启」改为「进入中断警告且不重启」,并补一条
reject 路径(此前只覆盖同步 throw)。

验证:pnpm test:unit(56 PASS / 0 FAIL)、desktop typecheck、check:i18n-glossary、
check:i18n 全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 09:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (2)

apps/desktop/src/renderer/i18n/locales/en/common.json:4151

  • 英文确认按钮文案目前是 "Restart App Anyway"(Title Case + 插入 App),与同一块 banner 里既有的 sentence case(例如 "Newer version found"、历史上的 "Restart now")不一致,也与 confirmAria 的 "Restart anyway…" 语气不一致。建议改为 sentence case 的 "Restart anyway"(或至少保持同一风格)。
      "confirmButton": "Restart App Anyway",

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:236

  • 这里对 busy 探针失败走 fail-closed(hasInFlight 初值 true,catch 里也强制 true),会把“桥不可用/IPC reject”等情况当作仍有任务在跑,从而额外进入一次 confirming 警告。PR 描述中写的是与 WindowControls.handleCloseClick 同构(WindowControls.tsx:90-95 的 catch→false),即探针失败按“没有任务”直接执行一次点击重启;两者口径不一致,需确认到底以 PR 描述还是以当前实现为准,并同步对齐注释与测试期望。
    let hasInFlight = true;
    try {
      hasInFlight = await window.electronAPI.anySessionInTurn();
    } catch {
      hasInFlight = true;

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd8dd0aa1d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx Outdated
@MagicLizi

Copy link
Copy Markdown
Contributor

@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

review 指出 anySessionInTurn() 查不到后台活动,这是改成单击直达后**新增**的
无保护场景 —— 旧流程无论如何都有第二次确认,用户至少能看到一屏并中止。

channels.ts 把「turn 已结束但 CC 子进程仍在调模型」(后台子 agent、
run_in_background 的 Bash) 维护成与逻辑 turn 完全独立的状态
(SESSION_BACKGROUND_ACTIVITY),anySessionInTurn 只看 SessionTurnActivityTracker
与 live session 的 isTurnRunning,两者互不覆盖。所以父 turn 刚结束、后台子 agent
还在跑时,点一下就会经 forceQuit() 直接 process.exit(0) 把它打断或留成失联进程。

改为并行查两个来源,任一为真就进中断警告态:
- anySessionInTurn()
- maker.listSessionBackgroundActivity()(preload 已有,renderer 侧现成 API)

Promise.all 任一 reject 仍落到 catch → fail closed,与上一版一致。

刻意未覆盖并已在注释与 PR 描述写明:scheduler「run 已启动但 turn 尚未开始」的
窄窗口。它没有 renderer 侧查询入口(main 的无人值守探针另走 SQLite
hasRunningRuns),且 run 真跑起来后会产生 turn,落回第一个来源。没有把它当成
已覆盖。

未采用「复用 main 侧无人值守重启探针」的建议:那套判定含远程 controller 与
remote invoke,对无人值守是对的,但手动重启是用户主动发起 —— 有远程设备在看
会话列表并不构成「会被打断的任务」,套过来会产生误报警告。

测试补两条并验证过能区分错误修法(临时移除后台活动查询后两条全红):
- 只有后台活动、无 turn → 进警告态且不重启,点确认才重启
- 后台活动查询 reject → fail closed

验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。首跑时
apps/mobile 的 startupSplashOverlay 用例 5s 超时,单独跑 733ms 通过、重跑全量
亦通过,是负载下的 flake,与本改动(desktop renderer)无关。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 09:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b13938639d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@MagicLizi

Copy link
Copy Markdown
Contributor

@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@dashhuang

Copy link
Copy Markdown
Member Author

⚠️ 合并前请先看这条:本 PR 有一个已确认但尚未修复的缺口,方向待定。

review threads 现在是 0 unresolved、CI 全绿,但这不代表改动已经完备。上一条 review thread 是按「已回应」resolve 的(auto-review 流程的判据),不是按「已修复」

当前实现的 busy 判定覆盖「逻辑 turn」与「Claude 后台活动」两个来源,没有覆盖 Ghost 的 card-action 后台活动ghostSessionActivityStore.ts 明确该链路不经 LLM turn,而 forceQuit() 里有 getGhostNodeRuntimeBroker().destroyAll())。也就是说插件正在生成媒体时点「立即重启」,仍会被无警告地打断。

根因不是漏了某个来源,而是「有哪些活动来源」在本仓没有单一事实源 —— 详见 PR 描述里的「⚠️ 待维护者定范围」与「已知债:仓内『什么算 busy』有五处不同定义」两节。三个候选方向(main 侧单一聚合探针 / 只补 Ghost 一个来源 / 把本 PR 缩回纯文案改进)已提交维护者决策,其中第一个需要按 docs/dev-rules/cindy-updater.md 授权才能动 main 侧更新链路。

方向确定并落地前,请不要合并。 落地结果会更新到本 PR 描述与对应 thread。

@MagicLizi

Copy link
Copy Markdown
Contributor

本 PR 命中 UI 路径但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范。

@dashhuang

Copy link
Copy Markdown
Member Author

UI 证据:UpdateBanner 三态 × Light / Dark(静态复现)

auto-review 提示本 PR 命中 UI 路径但缺界面证据。截图这条路走不通:dev 实例被 isDev() 短路(updateService.tsupdateChannelKind()),拿不到真实 ready 更新态,截不到真机画面。所以按提示给出的另一条路,附一份自包含 HTML 静态复现。

诚实标注边界:这不是实机渲染。颜色值逐个抄自 themes/builtin/cindy-{light,dark}.tsthemes/colors.ts,布局按 UpdateBanner.tsx 的 class 等价还原,火焰/spinner 是等价占位图形。本 PR 未新增任何颜色与样式——改的是「何时进入第二态」以及该态的文案。

三态里只有 ② 是本 PR 的可见变化(①③ 原样):

变化
① ready 未变。改的是点击后的行为:确认没有任务在跑就直接重启,不再无条件弹第二步
② 有任务在跑 本 PR 的可见变化:标题「仍有任务在运行中」、警告色副标题「重启会中断运行中的任务」、主按钮「仍要重启应用」;只在探到有任务、或探针拿不到可信答案时出现
③ superseding 未变

另附一条静态复现暴露的观察(非本 PR 引入,本 PR 不改):Light 模式下 --warning-fg#F3A115)落在侧栏底 #EDEDED 上,实算对比度约 1.8:1,低于 WCAG AA 对正文的 4.5:1;换 --warning-accent#EA6B17)实算约 2.7:1,仍不达 3:1。这个搭配在改动前的 confirmBusyHint 上就已存在、场景与频率都未被本 PR 加重,而 warning-fgDESIGN.md 标注的语义豁免 token(注释写明 2026-07-17 设计定稿)。按 DESIGN.md §10「缺少明确语义映射时先请设计裁决」,不在本 PR 内自行改色,只如实记录。

展开:自包含 HTML(存成 .html 双击即可看,无外部依赖)
<!doctype html>
<meta charset="utf-8">
<title>UpdateBanner — 更新重启入口三态 × Light / Dark</title>
<style>
  /* token 值抄自 apps/desktop/src/renderer/themes/builtin/cindy-{light,dark}.ts
     与 themes/colors.ts,未新增任何颜色。 */
  .light {
    --sidebar: hsl(0 0% 92.9%);
    --sidebar-border: hsl(214.3 11.1% 87.6%);
    --sidebar-muted: hsl(220 4.7% 62.2%);
    --foreground: hsl(214.3 5.5% 24.9%);
    --sidebar-item-hover: rgba(0,0,0,.05);
    --update-btn-bg: #3C3F43;
    --update-btn-border: #3C3F43;
    --update-btn-text: #FCFCFC;
    --warning-fg: #F3A115;
  }
  .dark {
    --sidebar: hsl(0 2.4% 16.1%);
    --sidebar-border: hsl(0 0% 26.3%);
    --sidebar-muted: hsl(0 0% 43.5%);
    --foreground: hsl(0 0% 83.1%);
    --sidebar-item-hover: rgba(255,255,255,.09);
    --update-btn-bg: #EEEEEE;
    --update-btn-border: #EEEEEE;
    --update-btn-text: #252222;
    --warning-fg: #F3A115;
  }
  body { margin:0; padding:24px; font:13px/1.5 -apple-system,"SF Pro Text","PingFang SC",system-ui,sans-serif;
         background:#6b6b6b; color:#fff; }
  h1 { font-size:15px; font-weight:600; margin:0 0 4px; }
  p.note { font-size:12px; opacity:.85; margin:0 0 20px; max-width:900px; }
  .grid { display:flex; gap:20px; flex-wrap:wrap; }
  .col { }
  .col > h2 { font-size:12px; font-weight:600; margin:0 0 8px; opacity:.9; }
  .sidebar { width:248px; background:var(--sidebar); border-radius:10px; overflow:hidden;
             box-shadow:0 1px 3px rgba(0,0,0,.35); }
  .filler { height:56px; }
  .banner { border-top:1px solid var(--sidebar-border); position:relative;
            display:flex; flex-direction:column; align-items:center; gap:10px; padding:12px 16px; }
  .x { position:absolute; right:6px; top:6px; width:24px; height:24px; border-radius:999px;
       display:flex; align-items:center; justify-content:center; color:var(--sidebar-muted); }
  .x svg { width:14px; height:14px; }
  .flame { color:var(--sidebar-muted); }
  .flame svg { width:36px; height:36px; }
  .title { font-size:14px; font-weight:600; color:var(--foreground); margin:0; }
  .sub { font-size:12px; text-align:center; color:var(--sidebar-muted); margin:0; }
  .sub.warn { color:var(--warning-fg); }
  .notes-link { font-size:12px; color:var(--sidebar-muted); text-decoration:underline;
                text-underline-offset:2px; }
  .pill { width:100%; box-sizing:border-box; display:flex; align-items:center; justify-content:center;
          gap:8px; border-radius:999px; border:1px solid var(--update-btn-border);
          background:var(--update-btn-bg); color:var(--update-btn-text);
          font-size:13px; font-weight:500; padding:8px 0; }
  .ghost { width:100%; box-sizing:border-box; display:flex; align-items:center; justify-content:center;
           border-radius:999px; padding:6px 0; font-size:13px; font-weight:500;
           color:var(--sidebar-muted); }
  .stack { display:flex; flex-direction:column; gap:8px; width:100%; }
  .spinner { width:36px; height:36px; border-radius:999px; border:2px solid var(--sidebar-muted);
             border-top-color:transparent; }
  .cap { font-size:11px; opacity:.8; margin:6px 0 0; text-align:center; }
</style>
<h1>UpdateBanner — 更新重启入口三态 × Light / Dark</h1>
<p class="note">
  静态复现,非实机截图:dev 实例被 <code>isDev()</code> 短路、拿不到真实 <code>ready</code> 更新态,
  所以无法截到真机画面。这里的颜色全部抄自 <code>themes/builtin/cindy-{light,dark}.ts</code><code>themes/colors.ts</code> 的 token 值,布局按 <code>UpdateBanner.tsx</code> 的 class 等价还原
  (图标为等价占位)。本 PR 未新增任何颜色与样式,改的是「何时进入第二态」与该态的文案。
</p>
<div class="grid" id="grid"></div>
<script>
  const STATES = [
    { key:'ready', label:'① ready(入口,未变)',
      title:'已更新到 1.2.3', sub:'重启以应用更新', warn:false, link:true,
      primary:'立即重启', cancel:null, spinner:false,
      cap:'点一下 → 探针确认没有任务在跑 → 直接重启(本 PR 的主改动)' },
    { key:'confirming', label:'② 有任务在跑(本 PR 的可见变化)',
      title:'仍有任务在运行中', sub:'重启会中断运行中的任务', warn:true, link:false,
      primary:'仍要重启应用', cancel:'取消', spinner:false,
      cap:'只在探到有任务 / 探针拿不到可信答案时出现;焦点默认落在「取消」' },
    { key:'superseding', label:'③ superseding(未变)',
      title:'检测到新版本', sub:'更新中…', warn:false, link:false,
      primary:'准备中', cancel:null, spinner:true,
      cap:'入口按钮 disabled,点击是 noop' },
  ];
  const FLAME = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2s4 4.5 4 9a4 4 0 0 1-8 0c0-1.5.5-2.5.5-2.5S6 11 6 14a6 6 0 0 0 12 0c0-5-6-12-6-12z"/></svg>';
  const XICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>';
  let html = '';
  for (const mode of ['light','dark']) {
    html += `<div class="col"><h2>${mode === 'light' ? 'Light' : 'Dark'}</h2>`;
    for (const s of STATES) {
      html += `<div class="${mode}" style="margin-bottom:18px">
        <div style="font-size:11px;opacity:.85;margin-bottom:6px">${s.label}</div>
        <div class="sidebar">
          <div class="filler"></div>
          <div class="banner">
            <div class="x">${XICON}</div>
            ${s.spinner ? '<div class="spinner"></div>' : `<div class="flame">${FLAME}</div>`}
            <p class="title">${s.title}</p>
            <div style="display:flex;flex-direction:column;align-items:center;gap:4px">
              <p class="sub${s.warn ? ' warn' : ''}">${s.sub}</p>
              ${s.link ? '<span class="notes-link">查看更新公告</span>' : ''}
            </div>
            ${s.cancel
              ? `<div class="stack"><div class="pill">${s.primary}</div><div class="ghost">${s.cancel}</div></div>`
              : `<div class="pill"${s.spinner ? ' style="opacity:.7"' : ''}>${s.primary}</div>`}
          </div>
        </div>
        <p class="cap">${s.cap}</p>
      </div>`;
    }
    html += '</div>';
  }
  document.getElementById('grid').innerHTML = html;
</script>

review 连续三轮指出同一形状的问题:又一个活动来源没被 busy 判定覆盖(逻辑 turn
→ Claude 后台活动 → Ghost card-action)。根因不是漏了某个来源,而是判定放在
renderer 侧逐个枚举,而「有哪些活动来源」在本仓没有单一事实源——每加一个来源就
漏一次,漏掉的后果是静默打断用户任务且不可撤销。

改成 main 侧一处判定,renderer 只问一次结论:

- 新增 relaunchBusyActivity.ts:纯函数聚合三个来源,依赖全注入。三源等价无主次,
  任一报忙即忙;**任一来源读取抛错也算忙**(「无法确认」不等于「确认没有」),
  标签带 -probe-failed 后缀以便在日志里区分「真有活动」与「探针坏了」。不短路,
  reasons 完整反映现场。
- GhostSessionActivityTracker 加 anySessionBusy():它此前只有 per-session 的
  isSessionBusy,而 renderer 侧 ghostSessionActivityStore 只靠 0↔1 推送累积、
  没有全量快照通道,首次订阅拿不到已在跑的活动,不能当权威来源。
- bootstrap-electron 注册 update-relaunch:blocking-activity:那是本进程唯一能同时
  看到 maker 与 cindy-brain 两侧跟踪器的位置。与既有的无人值守探针
  (setUpdateAutoRelaunchBusyProbe) 刻意分开——后者连「有远程设备在看会话」都让路,
  手动重启只该关心「这一下会打断哪些正在跑的活」,把远程 controller 纳进来只会
  产生误报警告。
- UpdateBanner 改为单次查询,renderer 不再枚举来源;新增来源以后只改 main 那一处。

device-link allowlist:不登记。allowlist 顶部注释把 updater 类列为「永不放行」,
且远程控制端不会代替用户点被控端的更新重启。

刻意未覆盖并已写进注释:scheduler「run 已启动但 turn 尚未开始」的窄窗口——纳入它
要引入 SQLite 异步查询,而 run 真跑起来会产生 turn、落回第一个来源。

未触及 updateService.ts 与 cindy-updater/:本 PR 只提供判定查询,不改重启执行路径,
因此不在 docs/dev-rules/cindy-updater.md 的门禁范围内。

测试:relaunchBusyActivity 9 条(三源各一条 + 多源并发 + 三源各自 fail-closed +
一源抛错不影响其它源)、ghostSessionActivity 补 anySessionBusy 4 条(含多会话引用
计数与 TTL 兜底)。renderer 侧删掉两条已搬走职责的用例,改为只测「拿到 true 就拦、
false 才走、拿不到就保守」这一契约。

验证:pnpm test:unit 56 PASS / 0 FAIL(desktop 60.3s、mobile 11.0s)、desktop
typecheck 通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 13:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f746a583f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/bootstrap-electron.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.

Suppressed comments (3)

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220

  • 这里写「有三个互不相干的来源」,但 main/relaunchBusyActivity.ts 实际聚合了 5 类来源(还包含后台 Bash 与 scheduler run)。建议避免在 renderer 侧写死来源数量/清单,改为明确“由 main 侧聚合判定”。
  // 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
  // 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
  // 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
  // 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。

apps/desktop/src/preload/preload.ts:2942

  • 注释里写的是「聚合三个来源」,但 main 侧实际还会把后台 Bash 与 scheduler run 计入 busy(apps/desktop/src/main/relaunchBusyActivity.ts:43-62)。建议更新注释,避免与真实行为不一致。
  /**
   * 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
   * Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
   * (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
   * 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。
   */

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11

  • 文件头注释仍在按「两个来源」描述 busy 判定,但现在 renderer 侧只调用 anyActivityBlockingRelaunch(),实际聚合范围已收敛到 main/relaunchBusyActivity.ts(包含 turn/Claude/Ghost/后台 Bash/scheduler run)。建议把这里的来源枚举改成指向 main 侧聚合判定,避免注释过时。

This issue also appears on line 217 of the same file.

 * 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
 * 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
 * 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。

mode:'submit' 的图片 / 视频生成由 cindySlot.ts 的 `void runExec()` 脱离调用链执行,
job 只活在 GhostCindySlot 私有的 jobs Map 里(无对外查询)。发起它的 turn 结束后,
既有五个来源全部返回空闲,而 forceQuit() 会连 Ghost Node runtime 一起 destroyAll,
正在生成的图 / 视频直接没了。

GhostCindySlot 新增 anyAsyncJobRunning(),接入聚合判定作为第六源。

同时修正上一轮对这个问题的定性。我此前把「判定不完备」上报成需要重新设计的阻塞项,
那个判断是错的:**改动前那次二次确认根本不做任何活动判定**(文案只有「应用会自动
重启」),对全部六类活动都是「点一下就 forceQuit」。所以

- 覆盖到的来源 = 净收益(从无警告变成有警告);
- 没覆盖到的来源 = 与改动前行为完全一致,不构成回归。

reviewer 说「旧流程还有第二次确认,所以这是改成单击后新增的无保护场景」——那次点击
不含任何活动信息,用户不知道有任务在跑,拦不住任何人,不构成保护。

据此在 relaunchBusyActivity.ts 文件头写明:这份清单不保证完备(异步活动持有者是开放
集合),但漏掉某个来源不是回归,发现新来源就往这里加一条。重启路径本身沿用既有逻辑,
一行未改。

测试:新增「Cindy slot 异步代办在途时阻断」,fail-closed 参数化用例扩到五个同步源,
多源并发用例覆盖六源全命中。

验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 13:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 539de40e7c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/cindy-brain/cindySlot.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

Suppressed comments (5)

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220

  • 这里的注释写「有三个互不相干的来源」,但 main 侧的 evaluateRelaunchBusyActivity 已扩展为更多来源(例如后台 Bash、Cindy slot 异步 job、scheduler run 等),并且强调“新增来源只改 main 侧那一个函数”。建议此处注释不要写死来源数量/枚举,以免再次过期。
  // 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
  // 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
  // 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
  // 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:428

  • 注释写 confirming 态“一律是『有任务在跑』才进来的”,但 handleRelaunchClick 对探针 reject/throw 也会 fail-closed 进入 confirming(此时实际是“无法确认”)。建议把注释改成“探针判定 busy(含 fail-closed)”以匹配真实语义。
          {/* confirming 态一律是「有任务在跑」才进来的,所以警告色与 busy 文案不再分支。 */}

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11

  • UpdateBanner 顶部注释仍在描述「逻辑 turn + Claude 后台活动」两来源、且写了“只有真的有任务时才进入确认态”,但当前实现已改为通过 anyActivityBlockingRelaunch() 走 main 侧多源聚合判定,并且探针失败也会 fail-closed 进入 confirming。建议把注释改成不枚举来源、并准确描述 fail-closed 语义,避免后续维护者按旧注释误改。

This issue also appears in the following locations of the same file:

  • line 217
  • line 428
 * 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
 * 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
 * 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。

apps/desktop/src/preload/preload.ts:2941

  • preload 侧 anyActivityBlockingRelaunch 的注释仍写“聚合三个来源”,但 main 侧判定已扩展为更多来源。建议这里改成引用 relaunchBusyActivity.ts 的“多源聚合”而不是写死数量,避免注释再次过期。
   * 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
   * Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
   * (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
   * 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。

apps/desktop/src/main/bootstrap-electron.ts:3820

  • 这里的注释写“四个活动来源的聚合”,但传入 registerRelaunchBusyActivityIpc 的 sources 实际上已有 6 个(turn / Claude / Ghost / 后台 Bash / Cindy slot job / scheduler run)。建议去掉数量描述或改成“多源聚合”,避免注释与实现不一致。
      // 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见

@MagicLizi

Copy link
Copy Markdown
Contributor

@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/main/cindy-brain/cindySlot.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

上一版加的 anyAsyncJobRunning() 只遍历异步 jobs Map,漏了同一个类里的 inflight
——那是同步代办的 per-ghost 在途计数(gen_image / gen_video 的同步等待、明确不进
会话的 oneshot_text)。插件面板发起的同步请求不一定伴随 turn 或 card-action,所以
其它探针同样不命中,首次点击就会 forceQuit() 掉正在生成的付费结果。

按 review 建议的第二种改法做:方法改名 anyInflightWork(),一次给出「所有 Cindy slot
在途工作」的统一快照(jobs 有 running 的 ∪ inflight 计数 > 0),而不是某一种。

顺带修正三处注释里的事实错误:异步提交(mode:'submit')**只对视频类开放**——
cindySlot.ts 第 449 行明确拒绝图像的 submit(「图像代办秒级完成,直接同步等待」)。
此前 cindySlot.ts、relaunchBusyActivity.ts 与提交说明都写成「异步图 / 视频生成」,
与实现不符。这个错误是写测试时被真实代码路径打回来才发现的:用 gen_image + submit
构造的用例返回 ok:false。

测试(走真实 handleModelRequest 路径,不直接摸私有 Map):
- 空闲为 false
- 同步代办在途期间为 true(在 generateImage 里断言),结算后回到 false
- 异步视频代办 mode:'submit' 受理后为 true

验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 14:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19edfef5e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/cindy-brain/cindySlot.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (4)

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220

  • 这里仍写“有任务在跑”只有三类来源,但 main 侧 relaunchBusyActivity.ts 当前聚合的不止三类(还包含 scheduler run / 后台 Bash / Cindy slot 在途代办等)。建议把注释改成“来源是开放集合,统一由 main 聚合”,避免注释与实现脱节。
  // 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
  // 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
  // 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
  // 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。

apps/desktop/src/preload/preload.ts:2942

  • preload 侧注释仍写“聚合三个互不相干的活动来源”,但 main/relaunchBusyActivity.ts 已扩展为 6 类来源聚合;注释与实现不一致,容易让调用方误判覆盖面。建议改成引用 main 侧聚合点(或更新为“多个来源”)。
  /**
   * 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
   * Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
   * (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
   * 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11

  • 这里的组件头注释仍按“逻辑 turn + Claude 后台活动(两源)”描述 busy 判定,但当前实现已改为通过 anyActivityBlockingRelaunch() 走 main 侧 relaunchBusyActivity.ts 的聚合判定(且该文件明确列出 6 类来源)。注释与真实语义不一致,后续维护容易误导。

This issue also appears on line 217 of the same file.

 * 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
 * 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
 * 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。

apps/desktop/src/main/bootstrap-electron.ts:3822

  • 这里的注释写“四个活动来源的聚合…”,但紧接着注册的 sources 实际提供了 6 个来源(turn/Claude/Ghost/后台 Bash/Cindy slot/scheduler)。建议把数量描述改成“多个来源”或直接引用 relaunchBusyActivity.ts,避免后续再扩展时注释长期漂移。
      // 手动更新重启(侧栏 UpdateBanner)的阻断判定。与上面那个**无人值守**探针刻意分开:
      // 无人值守要连「有远程设备在看会话」都让路,手动重启是用户主动发起的,只该关心
      // 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见
      // relaunchBusyActivity.ts,handler 与 sender 断言见 relaunchBusyActivityIpc.ts;
      // 这里只提供来源 —— 本进程唯一能同时看到 maker、cindy-brain 与 scheduler 三侧的位置。

review(P2)指出 deposit_media / release_media 在第 407 行就 return 了,走不到代办链
的 inflight 记账,所以寄存期间探针看不到;被 forceQuit() 打断会卡在 blob 落盘与账本
挂引用之间,留下孤儿 blob。

**没有照建议把它们计入 inflight**:那个 Map 同时是代办限流账(getInflightLimit,
超限返回「同时进行的代办已达上限」)。把面板里粘贴图 / 删素材算进去,会让这些操作
在限流触顶时被拒 —— 那是用户可见的行为变更,不该由本改动附带。改用独立的 mediaOps
计数,只服务于「重启会打断什么」的判定,限流语义一行未动。

释放放在 finally,抛错路径同样归零 —— 计数泄漏会让重启入口从此永久卡在「有任务在
跑」,比漏计更糟。

另外补一处上一个 commit 的不实之处:19edfef5 的说明写了「修正三处注释里的事实错误」,
但那次用来批量替换的脚本对 cindySlot.ts 那段没有加断言、静默没匹配上,实际只改到了
relaunchBusyActivity.ts。这次把 cindySlot.ts 的注释一并补齐(jobs = 异步**视频**代办,
inflight = 同步代办,mediaOps = 寄存),三处状态各自的边界都写清。

测试(走真实 handleModelRequest,不摸私有字段):
- 寄存在途期间为 true(在 depositMedia 内部断言),结算后回到 false
- 寄存抛错也归零(验证 finally)
两条都验证过能区分错误修法:移除 finally 后各自捕获到计数泄漏。

验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 14:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34c57a6924

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/bootstrap-electron.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (7)

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:220

  • 这里仍把阻断来源写成“三个来源”,但 anyActivityBlockingRelaunch() 的判定已在 main 侧聚合更多来源(例如 scheduler / Cindy slot / 后台 Bash 等)。建议避免在 renderer 侧注释里枚举来源,改为指向 main 侧单一事实源,防止来源清单继续漂移。
  // 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
  // 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
  // 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
  // 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11

  • 这里的头部注释仍描述为“两个来源都要看”,但实际实现已经改为调用 main 侧的聚合探针 anyActivityBlockingRelaunch()(聚合多个活动来源,见 relaunchBusyActivity.ts)。注释应与当前行为一致,避免后续维护时按旧语义误改。

This issue also appears on line 217 of the same file.

 * 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
 * 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
 * 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。

apps/desktop/src/preload/preload.ts:2942

  • preload 侧对 anyActivityBlockingRelaunch 的注释仍写“三个活动来源”,但 main 侧聚合判定当前包含更多来源(见 relaunchBusyActivity.ts)。建议把注释改成“聚合多个来源”并指向单一事实源,避免文档与实现漂移。
  /**
   * 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
   * Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
   * (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
   * 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。
   */

apps/desktop/src/main/bootstrap-electron.ts:3822

  • 这里的注释写“四个活动来源的聚合”,但 registerRelaunchBusyActivityIpc 传入的 sources 实际包含 6 类来源(turn / Claude 后台 / Ghost / 后台 Bash / Cindy slot / scheduler)。建议更新注释,避免后续读代码时误以为漏了一些来源。
      // 手动更新重启(侧栏 UpdateBanner)的阻断判定。与上面那个**无人值守**探针刻意分开:
      // 无人值守要连「有远程设备在看会话」都让路,手动重启是用户主动发起的,只该关心
      // 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见
      // relaunchBusyActivity.ts,handler 与 sender 断言见 relaunchBusyActivityIpc.ts;
      // 这里只提供来源 —— 本进程唯一能同时看到 maker、cindy-brain 与 scheduler 三侧的位置。

apps/desktop/src/main/bootstrap-electron.ts:3836

  • 这里把 Cindy slot 的异步 mode:'submit' 注释为“图 / 视频”,但 GhostCindySlot.handleModelRequest 会拒绝非视频类的 mode:'submit'(仅视频支持异步提交)。建议修正注释,避免误导后续把图片也当作异步来源之一。
        // Cindy slot 的全部在途工作:异步(mode:'submit' 的图 / 视频)与同步代办各自独立记账,
        // 都可能不伴随任何 turn 或 card-action,只查一半就漏一半。

apps/desktop/src/main/relaunchBusyActivity.ts:69

  • anyCindySlotJobRunning 这个来源名/标签与实际语义不一致:调用方传入的是 getGhostCindySlot().anyInflightWork()(包含同步 inflight、mediaOps 等),但在聚合判定里标记为 cindy-slot-async-job,会让日志 reasons 看起来像“只有异步 job 在跑”。建议把来源字段与 reasons 标签改成更贴近语义的命名(例如 anyCindySlotInflightWork / cindy-slot-inflight-work),并同步更新调用方与测试断言。
   * 是否有任意 Cindy slot 在途代办(异步 jobs + 同步 inflight 两半都算)。
   * **必须单独查**:两半各自独立记账、都可能不伴随 turn 或 card-action,而 forceQuit() 会连
   * Ghost Node runtime 一起销毁 —— 正在生成的付费结果直接丢掉。
   */
  anyCindySlotJobRunning: () => boolean;
  /**

apps/desktop/src/main/tests/relaunchBusyActivity.test.ts:110

  • 这里的测试注释写“mode:'submit' 的图片 / 视频生成”,但实现里 mode:'submit' 仅支持视频类(见 cindySlot.tsinfo.category !== 'video' 的拒绝)。建议改成“视频生成”,避免测试注释与实现语义冲突。
  // Cindy slot 异步代办(mode:'submit' 的图片 / 视频生成):void runExec() 脱链执行,只记在
  // GhostCindySlot 私有 jobs Map,发起 turn 结束后其它来源全看不到。

review 指出的问题成立,而且后果比 P2 的定级更重。

调用点 registerMakerIpcsAfterSplash 里,本 handler 注册在第 3826 行,而
makerIpcsRegistered = true 在第 3957 行;中间的 getMakerCore() 与
waitForInitialCustomMcpRefresh() 都可能抛,那个 try 的 catch 明写「不阻塞启动
—— 下次 splash retry 再尝试」。重试时 flag 仍是 false,早退不生效,于是这行被执行
第二次,ipcMain.handle 对同 channel 抛「Attempted to register a second handler」。

关键在于异常会从这里穿出去:不只是本 handler 注册失败,**排在它后面的全部 maker IPC
注册都被一起掀掉**,而且每次重试都卡在同一行 —— 结果是 maker 链路永久不可用。

修法用注册函数内幂等(先 removeHandler 再 handle),而不是把调用挪到成功之后:它对
调用位置不敏感,以后调用点顺序再变也不会重新踩坑。

测试:边界测试的 electron mock 改成仿真真实行为(同 channel 第二次 handle 直接抛)
—— 原来那个只 set 的假 Map 根本测不出重复注册。新增用例「重复注册不抛错且 handler
仍可用」,验证过能区分错误修法:移除 removeHandler 后该用例精确失败。

验证:pnpm test:unit 56 PASS / 0 FAIL、desktop typecheck 通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 14:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 471c6e1f95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/bootstrap-electron.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (7)

apps/desktop/src/main/relaunchBusyActivity.ts:106

  • probe('cindy-slot-async-job', ...) 的 reason label 会在日志/诊断里呈现,但当前来源实际覆盖的是 Cindy slot 的“全部在途工作”(异步 jobs + 同步 inflight + mediaOps),不只是 async job。建议把 label 改成更准确的名称(例如 cindy-slot-inflight-work),并同步更新相关测试期望。
    probe('cindy-slot-async-job', () => sources.anyCindySlotJobRunning());

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:221

  • 这里的注释仍把 busy 来源写成“3 个来源”,但 main 侧的聚合探针已包含更多来源(例如后台 Bash / Cindy slot 在途 / scheduler run)。建议要么列全,要么改成“不在 renderer 枚举来源,由 main 侧聚合”为准,避免注释过期。
  // 「有任务在跑」有三个互不相干的来源(逻辑 turn / Claude 后台活动 / Ghost card-action),
  // 判定收在 main 侧一处(relaunchBusyActivity.ts),这里只问一次结论。**刻意不在 renderer
  // 逐个枚举来源** —— 那样每加一个新来源就会漏一次(本 PR review 里连续被指出三轮),
  // 而漏掉的后果是静默打断用户任务。新增来源改 main 侧那一个函数即可,这里不用动。
  // 探针失败 = **无法确认**,不等于「没有任务」。重启会杀掉 in-flight turn,属于不可撤销的

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:11

  • 文件头注释里仍写“两个来源都要看”(逻辑 turn + Claude 后台活动),但实际阻断判定已经收敛到 main 侧并聚合了更多来源(如 Ghost card-action / 后台 Bash / Cindy slot 在途 / scheduler run)。这里的描述会误导后续维护。
 * 点「立即重启」不再无条件多要一次确认:先查「有没有任务在跑」(逻辑 turn + turn 已结束但
 * 仍在调模型的后台活动,两个来源都要看),**只有真的有任务时**才就地切换成确认态,确认没有
 * 就直接重启。原先那句「应用会自动重启」的中性二次确认纯属多一次点击、不带信息,已退役。

apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx:244

  • relaunchProbeRef 作为防重入标记只有在 Promise settle 的 finally 才会复位;如果 IPC handler 因某个来源(例如 scheduler 查询)挂起导致 anyActivityBlockingRelaunch() 永不 settle,这个入口会被永久锁死(后续点击全部 return)。建议为探针加一个超时兜底(超时按 busy/fail-closed 处理),确保 ref 一定能复位。
  const handleRelaunchClick = async (): Promise<void> => {
    if (relaunchProbeRef.current) return;
    relaunchProbeRef.current = true;
    const epoch = relaunchEpochRef.current;
    // 初值取 true:探针没给出可信答案的任何路径(reject、桥同步 throw)都落在保守的那一侧。
    let hasInFlight = true;
    try {
      hasInFlight = await window.electronAPI.anyActivityBlockingRelaunch();
    } catch {
      hasInFlight = true;
    } finally {
      relaunchProbeRef.current = false;
    }

apps/desktop/src/preload/preload.ts:2942

  • preload 侧注释仍写“三个活动来源聚合”,但 main 侧的阻断探针已经扩展为聚合更多来源(后台 Bash / Cindy slot 在途 / scheduler run 等)。建议同步更新注释,避免误导。
  /**
   * 现在重启会不会打断正在跑的活。聚合三个互不相干的活动来源(逻辑 turn / Claude 后台活动 /
   * Ghost card-action 后台活动),判定与 fail-closed 口径都在 main 侧一处
   * (relaunchBusyActivity.ts)—— renderer 逐个枚举来源会漏,漏了就是静默打断用户任务。
   * 供 UpdateBanner 决定「直接重启」还是「先弹中断警告」。

apps/desktop/src/main/bootstrap-electron.ts:3822

  • 这里的注释写“四个活动来源的聚合”,但 registerRelaunchBusyActivityIpc 实际注入了 6 个来源(含后台 Bash / Cindy slot / scheduler)。建议把数字去掉或改成准确的数量,避免注释过期。
      // 手动更新重启(侧栏 UpdateBanner)的阻断判定。与上面那个**无人值守**探针刻意分开:
      // 无人值守要连「有远程设备在看会话」都让路,手动重启是用户主动发起的,只该关心
      // 「这一下会打断哪些正在跑的活」。四个活动来源的聚合与 fail-closed 口径见
      // relaunchBusyActivity.ts,handler 与 sender 断言见 relaunchBusyActivityIpc.ts;
      // 这里只提供来源 —— 本进程唯一能同时看到 maker、cindy-brain 与 scheduler 三侧的位置。

apps/desktop/src/main/relaunchBusyActivity.ts:68

  • anyCindySlotJobRunning 的命名与注释/实际语义不一致:注释明确这是“异步 jobs + 同步 inflight 两半都算”(还包括 mediaOps),并非仅“job running”。命名不准会误导调用方与日志诊断。建议重命名为更贴近语义的名称(例如 anyCindySlotWorkInFlight),并同步调整注入方与测试。

This issue also appears on line 106 of the same file.

   * 是否有任意 Cindy slot 在途代办(异步 jobs + 同步 inflight 两半都算)。
   * **必须单独查**:两半各自独立记账、都可能不伴随 turn 或 card-action,而 forceQuit() 会连
   * Ghost Node runtime 一起销毁 —— 正在生成的付费结果直接丢掉。
   */
  anyCindySlotJobRunning: () => boolean;

@MagicLizi MagicLizi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review passed — well-architected relaunch detection with fail-closed semantics at every level, epoch-based probe invalidation, proper IPC sender verification, and comprehensive test coverage for all 6 activity sources.

@MagicLizi
MagicLizi merged commit b3a0888 into main Jul 31, 2026
14 of 15 checks passed
@MagicLizi
MagicLizi deleted the dash/update-relaunch-single-confirm branch July 31, 2026 15:16
@MagicLizi

Copy link
Copy Markdown
Contributor

六路活动探测加上 epoch 失效机制,既不放过正在跑的任务,也不拿假阳性烦用户——fail-closed 到位,已合入。

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.

3 participants