Skip to content

fix(desktop): 统一 device-link 项目的协同入口判定并补齐草稿链路 - #1212

Merged
MagicLizi merged 11 commits into
mainfrom
xdt/thoughtful-ritchie
Jul 31, 2026
Merged

fix(desktop): 统一 device-link 项目的协同入口判定并补齐草稿链路#1212
MagicLizi merged 11 commits into
mainfrom
xdt/thoughtful-ritchie

Conversation

@dashhuang

@dashhuang dashhuang commented Jul 31, 2026

Copy link
Copy Markdown
Member

这次改了什么

摘要

device-link(跨设备远程控制)项目的协同入口在「新建草稿」与「已创建会话」之间不一致(#1170):草稿里被硬编码关闭协同,发出第一条消息进入会话页后开关又出现了。根因是两处各写一份 eligible 判据,而判据分叉没有任何编译或测试信号。

排查中还发现更隐蔽的一层:协同的项目级 collab 开关一律查控制端本机。device-link 会话的 workingDir 是被控端机器上的路径,拿它在控制端自己的 fs 上找 .cindy/plugins.json 必然落空,读到的是控制端自己的用户级开关 —— 与被控端 main 的 assertCollabProjectEnabled(唯一权威授权)可能相反,入口据此置灰或放行都可能是错的,用户点下去才撞 PRECONDITION_FAILED

本 PR 按「device-link 支持协同」的方向补齐(会话页的远端 Worker 创建、团队读写、推送与 IPC allowlist 早已实现,缺的只是草稿阶段的开启链路与查询归属):

  • 新增 resolveCollabEntryPolicy,草稿路由与会话视图共用同一份判定,覆盖本地 / SSH 远端 / device-link / 对话模式 / Orca Worker 五类场景。
  • useCollabProjectPolicy 支持 deviceId,经新增的 makerTransport.pluginEnableStateFor 隧道到被控端读它自己的项目级真相;maker:plugins:get-state 登记进 device-link allowlist。老被控端回 CHANNEL_NOT_ALLOWED 时单独归类为 unsupported(置灰 + 专属文案,不挂一个永远不会成功的重试),与瞬时 unavailable 区分。
  • 解除草稿状态层对 device-link 的硬编码禁用,改为「换目标设备时清掉 Worker 富配置、保留协同开关」——model / providerId / effort / fast 都是设备作用域的,原样带到另一台机器会撞被控端的精确 preflight。
  • 草稿的「发送」与「新建目标」两条 device-link 分支在建会话后隧道 enableOrca 拉起 Worker,收敛进 remoteCollabHandoff enableOrca(Lead 的首个 turn 才带得上协同 MCP),但镜像回流 fire-and-forgetrefreshRemoteDeviceSessions 对瞬态错误有最长约 6.75 秒的退避重试,挡在交接前面会丢掉用户的首条消息或目标文案 —— 即 remoteSessionHandoff 第 33 轮 P1 那条不变量)。CreateWorkerPopoverdeviceId,模型清单来自被控端。

变更类型

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

范围

  • 关联 Issue / 需求:bug(desktop): device-link 项目的协同入口在草稿与已创建会话之间不一致 #1170
  • 本 PR 包含:协同入口判定收敛成共享 helper;协同策略查询按会话/草稿归属路由到被控端;maker:plugins:get-state 进 device-link allowlist;草稿两条 device-link 创建路径接通 enableOrca;换设备清 Worker 配置;unsupportedRemoteHint 四语文案;相关注释与 Orca 架构文档更新;对应单测。
  • 明确不包含:mobile 侧的协同开启入口(当前 apps/mobile 只渲染协同消息卡片,见下方「远程连接与手机版适配」);远端项目级 collab 配置机制(SSH 侧仍是 skipQuery,属既有 follow-up);协同 UI 本身的形态调整。
  • 用户可见变化:device-link 项目的新建草稿现在能看到并开启协同,配置 Worker 时模型/来源清单来自被控设备;草稿切换目标设备会清空已配的 Worker(协同开关保留);被控端项目关闭 collab 插件时,草稿与会话页入口同时置灰;被控设备版本过旧时给出「暂不支持协同」而不是含糊的重试提示。
  • 是否存在 breaking change:无

UI 变化

不涉及:本次没有新增或修改任何组件、布局、样式与动效,只复用既有 ChatInput collaboration prop 的 disabled / disabledReason 态,并让 device-link 草稿命中已有的协同 pill 渲染分支。新增的唯一用户可见文案是 newChat.collaboration.unsupportedRemoteHint(四语齐全,走既有 tooltip 通道,无新增颜色或硬编码样式)。

  • 引用的设计规范:不涉及(无视觉/交互变化;颜色与间距全部沿用既有组件,未引入任何硬编码)。

怎么验证的

自动验证

bash <git-skill>/scripts/run-unit-gate.sh <worktree>   # 仓库根 pnpm test:unit 全量
结果:GATE_EXIT=0(56 个 workspace PASS,apps/desktop unit 57.1s PASS)

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

pnpm --filter @cindy/device-link run build        # 该包的 build 即 tsc --noEmit
结果:通过

pnpm exec eslint <本次改动的 9 个文件>
结果:0 problems

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

pnpm check:i18n-glossary
结果:术语表无新增违规(复用已裁决的「协同」「被控设备」,未自造译法)

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

首轮全量门禁是 GATE_EXIT=1,暴露 3 个 source-guard 失败,全部真实且已修:

  • newMakerProjectPicker 守住「组件不得自己 import 回流函数」——由此把远程开协同的收尾抽成共享的 remoteCollabHandoff,两条草稿路径不再逐字重复;
  • orcaWorkflowRoute 守住「legacy /orca 路由必须被 !orcaMode && 短路掉」——判据收敛后同义更新断言;
  • 既有的 useCollabProjectPolicy 单测因 refresh() 返回值新增 unsupported 字段而失败——补齐断言,并把 device-link 相关用例并入该文件(避免出现第二份同名测试)。

新增 / 更新的测试:

  • collabEntryPolicy.test.ts(新增):五类场景 + 防「两个入口再次各写一份判据」的 drift 守卫;
  • useCollabProjectPolicy.test.ts:device-link 隧道路由、同路径不同设备不串台、CHANNEL_NOT_ALLOWEDunsupported 与瞬时失败 → unavailable 的分类;
  • newMakerDraft.test.ts:device-link 项目草稿可开协同、对话模式仍关、换设备清 workerConfig、同设备换项目保留;
  • newMakerOrcaCreateOrder.test.ts:两条 device-link 分支的 enableOrca 在交接之前、共用 helper、镜像回流不得被 await、按被控端目录收窄来源;
  • makerTransportRouting.test.tspluginEnableStateFor 本机 / 远程分流(新 channel 由既有 drift 守卫自动纳入 allowlist 校验)。

手工验证

不涉及(未做真机双端实测,原因见下)。

未执行的验证

  • device-link 双机实测未做:需要两台已互联的设备,本次没有这个环境。待验证路径:控制端 /cc-agent/new 选被控设备的项目 → 协同开关可见可开 → 配置 Worker(模型清单应来自被控端)→ 发送 → 被控端起 Lead + Worker、控制端协同 tab 能看到 Worker;切到另一台设备确认 Worker 配置被清空;被控端项目里关掉 collab 插件后控制端两处入口同时置灰;老版本被控端应给出「暂不支持协同」。
  • Light / Dark 双模式未实机目检:本次没有新增视觉组件,只复用既有 collaboration prop 的 disabled/tooltip 态,颜色全部走既有 themed 样式、无硬编码,但未在两种模式下实机目检。
  • 既有 lint 债未处理:useCollabProjectPolicy.tsprefer-constrequestPromise)在 main 基线上即已存在(已 stash 到基线复现确认),不属于本 PR 范围,未一并修改。

远程连接与手机版适配(docs/dev-rules/remote-and-mobile-adaptation.md 门禁)

  1. SSH 远程工作区:本 PR 不改 SSH 路径的行为。SSH 会话/草稿仍走 skipProjectQuery(远端主机路径在本机 fs 上查项目级既无意义又会误判,与 main 侧 assertCollabProjectEnabled 的 remote 分支同口径),判定逻辑只是从两处内联搬进了共享 helper。
  2. 新增 IPC channel:新增一个 device-link invoke —— maker:plugins:get-state,已按 packages/device-link/src/allowlist.ts 顶部的三条准入判据登记并写明理由(handler 只读 settings + 项目 .cindy/plugins.json,不依赖 event.sender、无 UI/shell 副作用,插件启停真相在被控端)。无新增推送事件,无 topic 路由变化。老被控端不在其 allowlist 内 → CHANNEL_NOT_ALLOWED → 控制端 fail-closed 置灰并说明原因,不会放行到 enableOrca 才撞错。
  3. 手机版入口:本 PR 不新增。apps/mobile 目前只渲染协同消息卡片(src/session/orcaCollab.ts),没有开启协同的入口;新 channel 对 mobile 无害(同一套 allowlist,需要时可直接复用)。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:

影响与回滚

  • 影响范围:device-link 项目的协同入口与协同策略查询;本地与 SSH 路径的判定逻辑等价搬迁(口径不变);packages/device-link 的 invoke 白名单新增一条只读 channel。不涉及服务端、不改 cindy-protocol submodule。
  • 协议兼容:新增 channel 是纯增量的。新控制端 → 老被控端:CHANNEL_NOT_ALLOWED,控制端 fail-closed 置灰并提示设备版本过旧;老控制端 → 新被控端:不会发起该调用,行为不变。两个方向都不需要版本协商。
  • 权限边界不放宽:被控端 main 的 assertCollabProjectEnabled 仍是唯一权威授权,本 PR 改的全部是 renderer 的入口体验层与查询归属。新增的 channel 只读、不含凭证材料,且 device-link 本身已是同账号 + remoteControlEnabled 显式 opt-in 的信任域内(控制端本就能在该 workingDir 跑 agent),不扩大攻击面。路径归一化 normalizeWorkingDirForProjectSettings 是纯路径形态推导(不依赖 process.platform / 本机 userData),跨 macOS ↔ Windows 控制同样成立。
  • 回滚 / 降级方式:整体 revert 即可,无数据迁移、无持久化格式变化。草稿里 collab.workerConfig 只是本地草稿态(本就不跨重启保留 initialTask),revert 后按旧规则重新收敛,不留脏状态。单点降级:把 resolveCollabEntryPolicy 里 device-link 的 eligible 改回 false,即可只关掉 device-link 的入口而保留其余修正。

提交前检查

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

不变量清单(review 收敛锚点)

十一轮 review 反馈全部落在同一族:device-link 草稿开启协同的时序与失败语义。逐条修容易漏掉对称的另一半,所以把不变量显式列出来,后续 review 请对着这张表看,而不是对着 diff 猜。

一句话不变量:device-link 草稿开启协同时,用户的输入必须先被登记并立刻交接出去;首轮必须排在协同定性之后;而「是否就绪」只能由被控端的权威状态回答。

第 4 轮的结论是前三轮那种「在导航前等」的形状本身站不住:它同时被两侧夹击 —— 等得越久,用户输入在内存里裸奔的窗口越长(greptile P1);等得不够久,又永远不是权威终态(codex P2)。所以第 4 轮改了结构:等待整体挪到导航之后,由 CCAgentSessionView 在消费 pending 时执行。新建页不再卡住,用户输入已经交到会话视图手里,而首轮仍由同一个 await 串在协同之后。

# 不变量 实现位置 守卫
1 提交点之后不得插入远程等待,可恢复副本也要紧贴提交点落下(排在 rehomeDraftAttachments 之前)。草稿路由登记完 setPending / setPendingGoal 就立刻 navigate,协同意图随 pending 载荷交接出去 NewMakerDraftRoute 两条 device-link 分支 + PendingRemoteCollab newMakerOrcaCreateOrder:draft route 不得出现 enableRemoteCollabForSession;副本排在提交点之后、附件迁移之前且只落一次
2 首轮必须排在协同定性之后。等待发生在会话视图消费 pending 时,sendMessage / setGoal 排在其后 CCAgentSessionView 两处 pending 消费 newMakerOrcaCreateOrder:两处消费共用 consumePendingRemoteCollab
3 归属判定唯一且粘滞。入口、策略查询、enable、disable、镜像回流、远程起目标的订阅判据六处同口径;只有一份粘滞缓存(视图不再自己记,统一走 stickySessionOrigin makerApiForSticky / getStickySessionDeviceId orcaRemoteRoutingInvariants:enable 与 disable 都不得退回非粘滞版;起目标的订阅判据必须与 goalApiFor 同口径
4 失败语义按性质分流。权威拒绝 → 立即降级;隧道超时 → 回查被控端权威终态;回查自身失败也分性质(瞬态用完剩余预算、永久立即降级、未知不空转);回查同时受次数与总时限两道闸(只限次数不限时间时,探针各自走满 30s 超时会把 composer 锁约 3 分钟);用尽 → fail-closed 降级 remoteCollabHandoffisAmbiguousTimeout / recoverTimedOutTeam / TIMEOUT_RECOVERY_DEADLINE_MS remoteCollabHandoff.test.ts 十条路径
5 镜像回流永不阻塞交接,且排在定性之后 remoteCollabHandofffinally newMakerOrcaCreateOrder:不得 awaitfinally 排在 return 之后
6 设备作用域的配置换设备必须重校。workerConfig 在换设备时清空;worker 类型在发送时按目标设备目录收窄(换掉则一并丢弃属于旧 agent 的 model / providerId) newMakerDraft.patchDraft / draftEnableOrcaOptions newMakerDraft.test.ts + newMakerOrcaCreateOrder:agent 收窄守卫
7 降级必须说清「这条仍会发出去」。_REMOTE 文案只讲去那台机器修,用户会以为没发而重复提交 getCollaborationStartErrorMessageremoteContinueNotice newMakerOrcaCreateOrder:两处消费都带 continueAsSingleSession
8 远程交接从登记那一刻起就有第二份副本,且只有确认交付成功才删——删除收敛为单一入口 deliverRecoverableHandoff(resolve true 删/false 保留/抛错保留并冒泡),forgetRecoverableHandoff 不再导出,裸调编译不过。落在消费处等于没落(消费 effect 要等 historyLoaded)。恢复只回填输入框、绝不自动补发,输入框已有内容时让路并保留副本;过期项剔除即写回 draft route 两条登记分支 + pendingFirstMessage.deliverRecoverableHandoff pendingHandoffRecovery.test.ts 18 例 + newMakerOrcaCreateOrder:副本落在 setPending* 之后 navigate 之前;SessionView 不得出现 forgetRecoverableHandoff(;三处交接都经 deliver
9 发送锁覆盖整条远程交接(消费 pending → 首轮发出 / setGoal 结束),不只是开协同那一段——subscribesetGoal 同样是隧道 invoke、同样可能 30s,锁外的窗口里补发消息会抢在首轮之前 remoteHandoffPreparing(刻意不叫 remoteCollabPreparing:也覆盖没开协同的远程起目标) newMakerOrcaCreateOrder:lock 早于两处 await、解锁晚于 sendMessage / setGoal

已枚举的对称路径(每条不变量都按这三个维度检查过):

  • 创建路径:草稿「发送」 × 草稿「新建目标」 —— 两条共用 remoteCollabHandoff,时序约束只有一处可改。
  • mutation:enableOrca × disableOrca —— 归属判定同口径。
  • 终态:成功 / 权威拒绝 / 超时但已建成 / 超时且未建成 / 回查自身失败 / 回流失败 —— remoteCollabHandoff.test.ts 逐条覆盖。

已知残差(刻意不做,非疏漏)

  1. 恢复只覆盖正文,不覆盖附件。第 7 轮补的可恢复副本只存文本:附件不落 localStorage,与本仓既有取舍一致(newMakerDraft 头部已定「附件 → 丢失(产品决策)」)。等待期间 app 被关掉,重开会话能拿回正文,附件需重新添加。
  2. 恢复不自动补发。回填到输入框后由用户自己决定是否再发 —— 上次按发送时的意图未必还成立,重启后凭空冒出一条已发消息比丢失更难解释。
  3. 回查的边界。被控端可能永远不返回,无界等待只会把首轮永久挂起,所以回查必须有界(当前 6 次 / 间隔 3s)。超出边界后按失败降级,同时仍刷一次镜像,让被控端稍后才提交的情况经 orcaRole 回流 + external-enable 边沿检测自愈,只影响首轮。

device-link 项目的协同入口在新建草稿与已创建会话之间不一致(#1170):草稿被
硬编码关闭协同,发出第一条消息进会话页后开关又出现。根因是两处各写一份 eligible
判据,判据分叉没有任何编译或测试信号。

还有更隐蔽的一层:协同的项目级 collab 开关一律查控制端本机 —— 拿被控端的路径在
自己的 fs 上找 .cindy/plugins.json 必然落空,读到的是控制端自己的用户级开关,与
被控端 main 的 assertCollabProjectEnabled 可能相反,入口据此置灰或放行都可能是错的。

本次按「device-link 支持协同」补齐:

- 新增 resolveCollabEntryPolicy,草稿路由与会话视图共用同一份判定,覆盖本地、
  SSH 远端、device-link、对话模式与 Orca Worker 五类场景。
- useCollabProjectPolicy 支持 deviceId,经新增的 makerTransport.pluginEnableStateFor
  隧道到被控端读它自己的项目级真相;maker:plugins:get-state 登记进 device-link
  allowlist(只读、无 event.sender 依赖、真相在被控端)。老被控端回
  CHANNEL_NOT_ALLOWED 时单独归类为 unsupported,置灰并给专属文案,不挂一个永远
  不会成功的重试。
- 解除草稿状态层对 device-link 的硬编码禁用,改为「换目标设备时清掉 Worker 富配置、
  保留协同开关」——model / providerId / effort / fast 都是设备作用域的。
- 草稿的发送与新建目标两条 device-link 分支在建会话后隧道 enableOrca 拉起 Worker,
  收敛进 remoteCollabHandoff:等 enableOrca(首个 turn 才带得上协同 MCP),但镜像
  回流 fire-and-forget(它的退避重试最长约 6.75 秒,挡在交接前面会丢掉用户的首条
  消息或目标文案)。CreateWorkerPopover 传 deviceId,模型清单来自被控端。
- 同步修正 main 侧与两个路由里「协同仅本地」的过时注释,以及 Orca 架构文档的边界段。

Signed-off-by: Dash <dashhuang@gmail.com>
@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 统一了 device-link 协同入口、远端策略查询和草稿交接链路。

  • 草稿页与会话页改为共用协同入口判定,并在被控设备上查询项目级插件状态。
  • device-link 草稿创建会话后,将协同初始化移至会话视图消费 pending 数据时执行。
  • 引入粘滞会话归属,统一远程协同启停、目标订阅和后续操作的设备路由。
  • 为首条消息和目标增加可恢复副本、完整交接锁及有界的超时终态回查。
  • 补充远程 IPC allowlist、兼容性提示、国际化文案及对应回归测试。

Confidence Score: 5/5

当前修订已处理显示的既有线程,未发现仍需阻止合并的故障。

当前代码统一使用同一份粘滞会话归属,并在远程等待前建立可恢复副本、在确认交付后删除;此前报告的首次瞬断误路由、等待期间丢失首条输入或目标,以及瞬断时跳过目标订阅均已由当前实现闭合,没有阻塞性失败残留。

Important Files Changed

Filename Overview
apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx 将远程协同等待、目标订阅、可恢复交付和发送锁集中到 pending 消费阶段,已覆盖此前线程指出的归属与输入丢失路径。
apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx device-link 草稿在创建远程会话后持久化交接正文并立即导航,同时携带协同意图供会话视图处理。
apps/desktop/src/renderer/state/pendingFirstMessage.ts 新增按用户隔离、带 TTL 的可恢复交接副本,并仅在确认交付成功后删除。
apps/desktop/src/renderer/features/cc-agent/remoteCollabHandoff.ts 统一远程协同初始化、超时终态回查、失败降级及非阻塞镜像刷新。
apps/desktop/src/renderer/lib/makerTransport.ts 新增远端插件策略查询并统一协同启停等操作的粘滞设备路由。
packages/device-link/src/allowlist.ts 将只读的项目插件状态查询加入 device-link 远程调用白名单。

Sequence Diagram

sequenceDiagram
  participant U as 用户
  participant D as NewMakerDraftRoute
  participant R as 恢复副本
  participant V as CCAgentSessionView
  participant C as 被控设备
  U->>D: 提交首条消息或目标
  D->>C: 创建远程会话
  D->>R: 保存可恢复副本
  D->>V: 登记 pending 并立即导航
  V->>C: 订阅 session topic
  V->>C: 查询并初始化协同
  V->>C: 发送首条消息或设置目标
  C-->>V: 返回交付结果与会话事件
  V->>R: 确认交付后删除副本
Loading

Reviews (11): Last reviewed commit: "fix(desktop): 订阅判据改粘滞、副本贴紧提交点、回查加总时限" | Re-trigger Greptile

@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: 6b2add9da7

ℹ️ 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/features/cc-agent/remoteCollabHandoff.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

该 PR 修复 desktop 端 device-link 项目在「新建草稿」与「已创建会话」之间协同入口判定不一致的问题,并补齐 device-link 草稿阶段开启协同的链路:将入口 eligible 判定收敛为单一 helper,同时将项目级 collab 开关查询归属正确路由到被控端(避免控制端误用本机开关导致入口误放行/误置灰)。

Changes:

  • 新增 resolveCollabEntryPolicy,让草稿路由与会话视图共用同一套协同入口判定,并明确区分 project / dialogue / worker 子会话等场景。
  • useCollabProjectPolicy 支持 deviceId:device-link 场景下通过 makerTransport.pluginEnableStateFor 隧道到被控端查询项目级开关;并将老被控端 CHANNEL_NOT_ALLOWED 单独归类为 unsupported(非可重试的 unavailable)。
  • device-link 草稿两条创建路径(发送 / 新建目标)在建会话后隧道 enableOrca 拉起 Worker(引入 remoteCollabHandoff),同时更新 allowlist、i18n 文案、文档与单测覆盖。

Reviewed changes

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

Show a summary per file
File Description
packages/device-link/src/allowlist.ts maker:plugins:get-state 加入 device-link invoke allowlist(只读查询)。
docs/dev-rules/orca-team-architecture.md 更新 Orca 架构文档,补充 device-link 协同入口与策略查询归属说明。
apps/desktop/src/renderer/state/newMakerDraft.ts 草稿切换目标设备时清理 Worker 富配置但保留协同开关;移除 device-link 草稿硬编码禁用协同。
apps/desktop/src/renderer/lib/makerTransport.ts 新增 pluginEnableStateFor:本机直连 IPC,device-link 走隧道查询被控端插件状态。
apps/desktop/src/renderer/i18n/locales/zh-CN/common.json 新增 newChat.collaboration.unsupportedRemoteHint 文案(zh-CN)。
apps/desktop/src/renderer/i18n/locales/en/common.json 新增 unsupportedRemoteHint(en)。
apps/desktop/src/renderer/i18n/locales/ja/common.json 新增 unsupportedRemoteHint(ja)。
apps/desktop/src/renderer/i18n/locales/ko/common.json 新增 unsupportedRemoteHint(ko)。
apps/desktop/src/renderer/features/cc-agent/remoteCollabHandoff.ts 新增 device-link 远程会话开启协同收尾 helper(await enableOrca + 回流 fire-and-forget)。
apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx 草稿路由接入统一入口判定;device-link 两条路径补齐远端 enableOrca;CreateWorkerPopover 传入 deviceId。
apps/desktop/src/renderer/features/cc-agent/hooks/useCollabProjectPolicy.ts 支持 device-link 隧道查询与 unsupported 状态;查询键包含 deviceId + workingDir 防串台。
apps/desktop/src/renderer/features/cc-agent/hooks/tests/useCollabProjectPolicy.test.ts 覆盖 device-link 隧道路由、不同设备同路径不串台、unsupported/unavailable 分类等。
apps/desktop/src/renderer/features/cc-agent/collabEntryPolicy.ts 新增协同入口单点判定 helper(五类场景 + 查询归属提示)。
apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx 会话视图改用 resolveCollabEntryPolicy;项目级查询按 device-link/SSH 场景路由并展示 unsupported 提示。
apps/desktop/src/renderer/tests/orcaWorkflowRoute.test.ts 更新 legacy /orca 路由的 drift/invariant 断言以匹配新判定结构。
apps/desktop/src/renderer/tests/newMakerProjectPicker.test.ts 确认回流逻辑不回流到组件内联,新增 helper 仍满足守卫。
apps/desktop/src/renderer/tests/newMakerOrcaCreateOrder.test.ts 覆盖 device-link 两条草稿路径 enableOrca 时序与不 await 回流等不变量。
apps/desktop/src/renderer/tests/newMakerDraft.test.ts 覆盖 device-link 草稿协同开启/对话模式仍禁用/换设备清 workerConfig 等 store 行为。
apps/desktop/src/renderer/tests/makerTransportRouting.test.ts 覆盖 pluginEnableStateFor 本机/远程分流与 skipQuery 透传。
apps/desktop/src/renderer/tests/collabEntryPolicy.test.ts 新增对 resolveCollabEntryPolicy 五类场景与“两个入口必须共用 helper”的 drift 守卫。
apps/desktop/src/main/maker-ipc/collabProjectPolicy.ts 补充注释:device-link 在 main 侧无需特判,授权仍以执行端为准。

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

被控端 maker:create-session 返回 sessionId 那一刻就是提交点。之前把远程
enableOrca 放在 setPending / setPendingGoal 之前,而它是一次隧道往返 ——
被控端起 Worker 本就慢,还可能一路走到 invoke 默认 30s 超时。于是「对端会话
已建好、用户的首条消息或目标文案还没被登记」的窗口被从一次本地 rehome 撑到
半分钟;窗口内应用被关掉,用户重开重试就会在对端建出第二个会话,第一个空着
滞留。这与 remoteSessionHandoff 第 33 轮 P1 是同一条不变量。

改为卡在 setPending / setPendingGoal 之后、navigate 之前:首条消息由
CCAgentSessionView mount 后 consumePending 发出,而它要等 navigate,所以
Lead 的第一个 turn 仍然带得上协同 MCP;setPending 只是往内存 Map 放一份
payload,不会提前触发发送。

镜像回流同时挪进 finally:控制端的 invoke 超时不会取消被控端正在跑的
enableOrca,「控制端报失败、对端稍后仍建成 team」是真实终态,失败路径也刷一次
让 orcaRole 尽快回流,由 external-enable 边沿检测把协同 tab 补开。

守卫测试同步改成断言修正后的时序(原断言方向写反了),并新增一条覆盖
finally 回流。

Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 10: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: 34fc717ede

ℹ️ 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/features/cc-agent/CCAgentSessionView.tsx

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 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (2)

apps/desktop/src/renderer/features/cc-agent/collabEntryPolicy.ts:53

  • nonEmpty() 目前用 trim() 做了非空判断,但返回的是原始字符串;如果上游传入带前后空格的 deviceId/remoteHostId,会导致 policyDeviceId / skipProjectQuery 的键不稳定(同时与 main 侧普遍的 .trim() 归一化不一致)。建议直接返回 trim 后的值,保证判定与缓存键稳定。
function nonEmpty(value: string | null | undefined): string | null {
  return typeof value === 'string' && value.trim() !== '' ? value : null;
}

apps/desktop/src/renderer/features/cc-agent/hooks/useCollabProjectPolicy.ts:70

  • requestedDeviceIdtrim() 只做了非空判断但未归一化返回值;如果 deviceId 带前后空格,会导致 requestKey 与实际 IPC 目标不一致(main 侧会 deviceId.trim()),从而产生重复查询/缓存分片。建议这里也直接使用 trim 后的 deviceId。
  const requestedDeviceId =
    eligible && typeof opts?.deviceId === 'string' && opts.deviceId.trim() !== ''
      ? opts.deviceId
      : null;

入口与协同策略查询按粘滞 remoteDeviceId 指向被控端,但两个 mutation 走的是
非粘滞的 makerApiFor(只读 getSessionDeviceId)。relay 瞬时重连清空
remoteProjectsStore 的窗口内,用户看到的入口分明指向被控端,点下去却会退回
本机 enableOrca / disableOrca —— 在控制端建出或销毁一个 team;本机恰好存在
同 id 会话时还会操作错对象。开启后的镜像回流同样用非粘滞判定,该窗口内会解析
成 undefined 直接跳过,被控端刚建的 worker 永远进不了控制端注册表。

归属判定收进传输层:新增 makerApiForSticky(与 isRemoteSessionSticky 同一判据,
一个服务 gating、一个服务调用),requestEnableCollab 与 useStopOrcaCollab 两条
对称路径一并改;回流改取 getStickySessionDeviceId。

send / setModel 这类高频操作仍用 makerApiFor:它们跟随实时判定,误判的代价是
一次失败重试,不是在错误的机器上留下持久状态。

守卫覆盖:makerApiForSticky 在瞬断窗口内仍走隧道、从未解析过归属的本机会话
零回归;enable/disable 两条路径不得退回非粘滞版;归属判定只住在传输层一处。

Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 11:02

@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: ce53a605bf

ℹ️ 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/features/cc-agent/NewMakerDraftRoute.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 23 out of 23 changed files in this pull request and generated no new comments.

Suppressed comments (1)

apps/desktop/src/renderer/features/cc-agent/hooks/useCollabProjectPolicy.ts:123

  • 这里的注释写“隧道回 CHANNEL_NOT_ALLOWED”,但 renderer 侧通过 extractIpcError 能识别到的实际错误码是 main 端映射后的 DEVICE_LINK_CHANNEL_NOT_ALLOWED(见 apps/desktop/src/main/device-link/ipc.ts 的 DEVICE_LINK_CODE_MAP)。建议把注释改成与实际可观察到的错误码一致,避免后续排障时按错码搜。
        // 老被控端没收录 maker:plugins:get-state → 隧道回 CHANNEL_NOT_ALLOWED。这是
        // **确定性**的不支持,不是瞬时故障:同 getWorkflowProgressFor 的判别方式,单独
        // 分类,让 UI 不去挂一个永远不会成功的重试。

device-link 隧道超时只删掉控制端的等待项,并不取消被控端仍在执行的 enableOrca
(ipc.ts 对 INVOKE_TIMEOUT 的既有判定就是「远端仍存活」)。之前的 catch 把
DEVICE_LINK_TIMEOUT 和 PRECONDITION_FAILED / INVALID_PARAMS 这类权威拒绝一视同仁,
吞错后立刻 navigate,于是「被控端起 Worker 慢了几秒」变成「用户明确开了协同,
首轮却以普通单会话跑」—— 而这恰是把 enableOrca 排在 navigate 之前要保住的东西。

改为按错误性质分流:权威失败原样抛出降级;超时先回查被控端 DB 的权威终态,
worker 已落库就照成功返回并带上 reveal,查不到才抛原始超时。回查有限次
(3 次 / 间隔 2s)且失败即放弃,fail-closed,绝不把「没建成」猜成「建成了」;
探针用 listWorkersByLead 而非 getByLeadSession —— 非空列表同时证明 team 与首个
Worker 都已提交,还顺带拿到 reveal 需要的 workerSessionId。

镜像回流仍在 finally,因此排在定性之后:成功路径上 enableOrca 返回即代表 DB
已提交,回查路径上已确认 worker 落库,判定为没建成时也仍刷一次,让被控端在回查
窗口之后才提交的情况经 orcaRole 回流 + external-enable 边沿检测自愈。

传输层补 orcaWorkflowsForDevice(与 makerApiForDevice 同款):调用方手里已有权威
deviceId 时不重新解析易失的 session origin。

新增 remoteCollabHandoff 行为测试覆盖成功 / 权威失败不回查 / 超时已建成 /
超时未建成 fail-closed / 回查自身失败 / 回流失败不影响返回值六条路径。退避用
fake timers 快进,避免用例真睡 4s 逼近 vitest 默认超时。

Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 11:26

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 24 out of 24 changed files in this pull request and generated no new comments.

Comment thread apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx
Comment thread apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx Outdated

@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: 69fe0eb036

ℹ️ 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/features/cc-agent/remoteCollabHandoff.ts
Comment thread apps/desktop/src/renderer/state/newMakerDraft.ts
Comment thread apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx Outdated
@MagicLizi

Copy link
Copy Markdown
Contributor

@dashhuang 👋 这个 PR 还有 5 条 review conversation 没 resolve(apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx / apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx / apps/desktop/src/renderer/features/cc-agent/remoteCollabHandoff.ts / apps/desktop/src/renderer/state/newMakerDraft.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。

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

前三轮把「等 enableOrca」放在导航之前的形状本身站不住 —— 它被两侧同时夹击:
等得越久,用户输入在内存 pending Map 里裸奔的窗口越长(隧道往返可能走到 invoke
默认 30s 超时,窗口内应用被关掉就永久丢消息、对端留下空会话);等得不够久,
空的 worker 列表又从来不是仍在执行的 enableOrca 的权威终态。

改结构:草稿路由登记完 setPending / setPendingGoal(载荷带上 remoteCollab)就
立刻 navigate,等待整体挪进 CCAgentSessionView 的 pending 消费 —— 先 await 开
协同、再发首轮 / 起目标。新建页不再卡住,用户输入已经交到会话视图手里,而首轮
仍由同一个 await 串在协同之后。等待不再占着 UI 后,超时回查也放宽到 6 次 / 3s,
但仍有界:被控端可能永远不返回,无界等待会把首轮永久挂起。

同轮修掉三处对称缺口:

- 粘滞归属只留一份缓存。视图原本自己记一份 ref,而 makerApiForSticky 读的是模块级
  stickySessionOrigin,后者只在被查询时预热 —— 用户在首次开启协同之前撞上 relay
  瞬断,那份还是冷的,mutation 就会退回本机、在控制端建出 team。视图改走同一份。
- Worker 类型也按目标设备目录收窄。换设备只清了 workerConfig,collab.worker 仍是
  旧值,切到没有该 agent 供应商的设备必撞 NO_PROVIDER_FOR_AGENT;现按 live 目录
  换用可用 agent,并丢弃属于旧 agent 的 model / providerId。
- 降级提示说清「这条仍会发出去」。_REMOTE 文案只讲去那台机器修好再重试,用户会
  以为没发而重复提交;补一句独立说明,不铺一套 _REMOTE_CONTINUE 文案。

Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 11:59
Comment thread apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (2)

apps/desktop/src/renderer/state/pendingFirstMessage.ts:240

  • __clearAllForTest() 里先把 activeDataOwnerId 置空再 removeItem(RECOVERY_STORAGE_KEY),会导致按 owner 分命名空间写入的 ${RECOVERY_STORAGE_KEY}:<owner> 残留(清理的 key 与实际写入的 key 不一致)。建议在清空 owner 之前先计算当前 recoveryStorageKey() 并一并删除,避免测试/调试清理不彻底。
  goalMap.clear();
  activeDataOwnerId = null;
  try {
    window.localStorage.removeItem(RECOVERY_STORAGE_KEY);
  } catch {

apps/desktop/src/renderer/state/pendingFirstMessage.ts:157

  • 这里的注释写的是“读全表并顺手剔除过期项”,但 readRecoveryTable() 只是过滤后返回,并不会把剔除结果写回 localStorage(过期项会一直留到下一次写入时才会被清掉)。建议把注释改成“过滤过期项”以免误导。

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

/** 读全表并顺手剔除过期项。localStorage 不可用 / schema 损坏时静默回退空表。 */

@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: 84704c5a04

ℹ️ 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/features/cc-agent/remoteCollabHandoff.ts Outdated
Comment thread apps/desktop/src/renderer/state/pendingFirstMessage.ts Outdated
本轮三条 review 反馈归为两族。

一、可恢复副本必须真的做到它声称的事
- 起目标那条路径把副本落在了 `deviceLink.subscribe` **之后**。subscribe 同样是隧道
  invoke、同样可能走到 30s 超时,而 `consumePendingGoal` 早就删了内存那份 —— 这段
  窗口目标正文照样会丢。改为在任何远程等待之前就落副本,判据也从「开没开协同」
  换成「是不是一次远程交接」:否则非协同的 device-link 起目标仍有同一个缺口。
- 过期项只在读取时被过滤掉,从没写回磁盘。该账号只要不再写新交接项,正文就无限期
  留在 localStorage 里,与声明的 TTL 和持久数据生命周期不符。改为剔除即写回,
  整份 JSON 损坏时同样覆盖掉。

二、超时回查的重试预算要按错误性质花
触发回查的前提就是链路刚抖过(enableOrca 超时),第一次回查撞上同一段抖动是常态。
原来只要回查抛错就立即 return null,等于「因为链路抖所以判定协同没建成」,后面五次
预算一次没用。改为:瞬态错误继续用完剩余轮次,永久错误(被控端版本过旧 / 远程已禁用)
立即降级,未知错误按不可重试处理。判据复用 device-link 既有的 isTransientRemoteError
—— 同一条传输、同一类判断,另写一份等价物正是本 PR 一路在消灭的形状。

测试:回查新增 4 条路径(瞬态后恢复成功 / 瞬态耗尽 fail-closed / 永久立即降级 /
未知不空转),副本新增 4 条(过期真的离盘 / 不牵连有效条目 / 损坏 JSON 被覆盖 /
顺序守卫覆盖 subscribe)。仓库根 pnpm test:unit 全量通过,desktop typecheck 通过。

Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 13:21

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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (2)

apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx:2768

  • pendingGoal 交接里用的是非粘滞的 getSessionDeviceId;在 device-link relay 瞬断清空注册表的窗口内这里会把远程会话误判成本机,从而跳过 deviceLink.subscribe(以及上面的可恢复副本判据也会变窄),与本文件其余位置改用 getStickySessionDeviceId 的口径不一致。建议这里同样改用粘滞归属。
        const deviceId = getSessionDeviceId(sessionId);

apps/desktop/src/renderer/state/pendingFirstMessage.ts:283

  • __clearAllForTest 会先把 activeDataOwnerId 置空再 removeItem(RECOVERY_STORAGE_KEY),但 rememberRecoverableHandoff 在设置过 owner 后会写入 ${RECOVERY_STORAGE_KEY}:<owner> 命名空间;这种情况下测试清理不会删掉实际写入的 key,可能导致后续用例读到脏数据。建议先按当前 owner 计算一次 recoveryStorageKey() 并同时清理两种 key。
export function __clearAllForTest(): void {
  map.clear();
  goalMap.clear();
  activeDataOwnerId = null;
  try {

@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: 6694139064

ℹ️ 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/features/cc-agent/CCAgentSessionView.tsx Outdated
Comment thread apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx Outdated
@MagicLizi

Copy link
Copy Markdown
Contributor

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

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

一、副本要在登记时落,不能等消费(codex P1)
上一版把 `rememberRecoverableHandoff` 放在 SessionView 消费 pending 处,而那条
effect 要等 `historyLoaded`。被控端此刻离线、或首次拉历史超过 PENDING_TTL_MS(60s)
时,effect 根本轮不到执行,内存项却已被 TTL 删掉 —— 链路恢复后 consumePending 只能
拿到 null,磁盘上也从来没写过,正文照样永久丢失。这是**消费之前**的窗口,与前两轮
修的「消费之后」不是同一个。
改为在草稿路由登记 `setPending` / `setPendingGoal` 的同一刻落副本,窗口从「消费之后」
提前覆盖到「登记之后」的全程;SessionView 不再自己落。

二、发送锁要覆盖整条远程交接(codex P2)
起目标的锁只包住开协同那段:前面的 `subscribe`、后面的 `setGoal` 同样是隧道 invoke、
同样可能走到 30s 超时,却都在锁外 —— 这两个窗口里用户补发的消息会抢在目标首轮之前。
首条消息那条有对称缺口:锁在命令派发那次 await 之前就释放了。
两条都改为锁覆盖「消费 pending → 首轮发出 / setGoal 结束」的全程。

顺带把 `remoteCollabPreparing` 改名 `remoteHandoffPreparing`:它现在也覆盖没开协同的
远程起目标路径,继续叫 collab 会让下一个人以为只在开协同时为真。

守卫相应改写:副本改为断言落在 draft route 的两条登记之后、navigate 之前,并断言
SessionView 不再出现 rememberRecoverableHandoff;锁改为断言 lock 早于 subscribe 与
collab 两处 await、解锁晚于 sendMessage / setGoal。仓库根 pnpm test:unit 全量通过,
desktop typecheck 通过。

Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 14:06

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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (3)

apps/desktop/src/renderer/tests/remoteSessionSyncInvariants.test.ts:76

  • 源码里 ChatInput.disabled 建议改为使用 sessionHandoffPreparing 后,这里的不变式断言也需要同步更新,避免测试继续锁定旧字符串。
    // device-link 远程交接期间也要禁用(见 remoteHandoffPreparing):那几段 await
    // 可能数十秒,不禁用的话用户补发的消息会插到草稿提交的首条之前。断线这一档不变。
    expect(sessionViewSrc).toContain(
      'disabled={remoteSessionUnavailable || remoteHandoffPreparing}',
    );

apps/desktop/src/renderer/tests/newMakerOrcaCreateOrder.test.ts:125

  • 同上:如果 ChatInput.disabled 改为 remoteSessionUnavailable || sessionHandoffPreparing,这里的字符串断言也要跟着更新,否则会把实现锁回 remoteHandoffPreparing
    // 「会话正在准备」只允许有一个下游判据:handleSend 拦截读合并值,ChatInput 也禁用。
    expect(sessionViewSource).toContain(
      'const sessionHandoffPreparing = worktreePreparing || remoteHandoffPreparing;',
    );
    expect(sessionViewSource).toContain('if (sessionHandoffPreparing) return false;');
    expect(sessionViewSource).not.toContain('if (worktreePreparing) return false;');
    expect(sessionViewSource).toContain(
      'disabled={remoteSessionUnavailable || remoteHandoffPreparing}',
    );

apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx:3464

  • ChatInputdisabled 仍只依赖 remoteHandoffPreparing,没有复用上方合并后的 sessionHandoffPreparing(worktree + remote handoff)。这会让“会话正在准备”的判据再次分叉,和注释里“下游只读这一个值”的意图不一致。建议直接改为使用 sessionHandoffPreparing
                  disabled={remoteSessionUnavailable || remoteHandoffPreparing}

@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: e221ff1097

ℹ️ 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/features/cc-agent/CCAgentSessionView.tsx Outdated
第 10 轮反馈仍是同一族,所以按收敛止损规则不再逐点打补丁,改为把生成这些缺口的
形状本身修掉。

本轮的 P1:`sendMessage` 在设备离线 / 访问被撤销 / 远端 maker:input:enqueue 拒绝时
**不抛错**,而是 resolve false,并且对远程会话还会把那条乐观气泡从 transcript 里撤掉。
之前没 await 就丢副本,等于正文从界面和磁盘上同时消失 —— 而内存 pending 已消费、
新建页草稿也已清空。

根因不是这一处漏了 await:"正文已经有新归宿了吗"这个判断此前散在三个调用点
(命令派发 / 首轮发送 / 起目标),各自就地判断,而这一族 review 每轮都是其中某一处
判错。所以收敛成单一入口:

- `forgetRecoverableHandoff` 不再导出,改为模块私有;
- 新增 `deliverRecoverableHandoff(sessionId, deliver)` 作为删除副本的唯一途径:
  resolve true → 丢副本;resolve false → 保留;抛错 → 保留并向上冒泡;
- 三处交接全部改走它。

于是"什么时候可以删"只剩一处可改,裸调 forget 直接编译不过(TS 已验证)。
守卫也从"forget 排在交出去之后"改成"会话视图里不得出现 forgetRecoverableHandoff、
且三处交接都经 deliver"。

测试:新增 3 例交付语义(resolve false 保留 / 抛错保留且冒泡 / resolve true 丢弃)。
仓库根 pnpm test:unit 全量通过,desktop typecheck 通过。

Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 14:27
Comment thread apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (1)

apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx:2684

  • 这里的发送锁(remoteHandoffPreparing)目前只在 pending.remoteCollab 非空时才会开启,但注释写的是“远程交接才上锁”。device-link 草稿即使没开协同,首轮 sendMessage 仍是隧道 invoke,可能长时间 await;若不锁住 composer,用户在会话首屏空白期间补发会插队到草稿首轮之前,造成顺序倒置。建议用“是否远程会话(粘滞归属)”作为判据,而不是“是否携带 remoteCollab”。
      // 锁要覆盖**整条交接**(消费 pending → 首轮发出),不能只包住开协同那段 await:
      // 解锁后到 sendMessage 之间还有一次 await(命令派发),那个窗口里用户补发的消息
      // 会抢在草稿提交的首条之前。远程交接才上锁 —— 本机交接没有远程等待。
      const holdComposer = !!pending.remoteCollab;
      if (holdComposer) setRemoteHandoffPreparing(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: 47a885dd5f

ℹ️ 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/features/cc-agent/remoteCollabHandoff.ts
Comment thread apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx Outdated
三条反馈分属三条既有不变量,各补一处漏网:

一、不变量 #3(归属判定唯一且粘滞)的第三处漏网
远程草稿起目标时,「要不要订阅 session:<id>」用的是非粘滞 getSessionDeviceId,而真正
发 setGoal 的 goalApiFor 走粘滞归属。relay 瞬断清空注册表的窗口内,订阅被跳过、setGoal
照样发到被控端 —— 目标首轮的 maker:event/status 推送落在没有订阅者的窗口里。改为
getStickySessionDeviceId,判据与执行端归属同口径。

二、不变量 #1(提交点之后不得插入等待)对副本同样成立
发送分支的副本落在 rehomeDraftAttachments 之后。那是本机 IPC,但含 base64 / 草稿缓存
图片时并不快;提交点之后每多一次 await,「对端会话已建好、正文却还没有第二份」的窗口
就长一分。副本只存正文、不依赖附件迁移结果,所以移到紧贴 commitRemoteSessionHandoff。
起目标分支的提交点与 setPendingGoal 之间没有 await,原位置已等价,只补注释说明。

三、回查只限次数不限时间
链路「可连但每个 invoke 都黑洞」时,每次 listWorkersByLead 自己就要走满 30s 隧道超时,
6 次串行 ≈ 3 分钟,而这段时间 composer 是锁住的。次数上限只约束了退避总和(15s),
没约束探针自身耗时。加 30s 总 deadline,两道闸谁先到都停:回查最坏 30s 收尾,叠加
最初 enableOrca 的 30s,composer 最坏锁约 1 分钟而不是 3 分钟。

顺带说明未做的:CCAgentSessionView 里还有三处非粘滞 getSessionDeviceId(extraDirs 写、
forkStripEncrypted 后的注册表重拉、vendor↔model 回退跳过),同样有瞬断窗口问题,但都是
本 PR 未触及的既有代码、且分属无关功能线,按「一个 PR 只做一件事」不并入,已在 thread
里说明并建议另开 issue。

测试:回查新增探针黑洞时按 deadline 收尾一例;守卫新增订阅判据必须粘滞、副本必须排在
提交点之后与附件迁移之前且只落一次。仓库根 pnpm test:unit 全量通过,typecheck 通过。

Signed-off-by: Dash <f9dftwf5tj@privaterelay.appleid.com>
Signed-off-by: Dash <dashhuang@gmail.com>
Copilot AI review requested due to automatic review settings July 31, 2026 14:45

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 30 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (2)

apps/desktop/src/renderer/features/cc-agent/remoteCollabHandoff.ts:98

  • recoverTimedOutTeam 的 overall deadline 检查只发生在 loop 顶部;当 attempt > 0 时会先 await delay(3000) 再发起 probe,这可能导致在 deadline 已到/即将到时仍额外等待并继续 probe,从而突破这里注释承诺的“谁先到都停”的总时限语义。建议在 delay 之后、发 probe 之前再做一次 deadline 检查(或按 remaining time 决定是否 delay)。
    if (attempt > 0) await delay(TIMEOUT_RECOVERY_DELAY_MS);

apps/desktop/src/renderer/state/pendingFirstMessage.ts:313

  • __clearAllForTest 目前只 remove 了未分命名空间的 RECOVERY_STORAGE_KEY,但该模块实际会在 setPendingHandoffOwner() 后把 key 变成 ${RECOVERY_STORAGE_KEY}:<owner>(见 recoveryStorageKey())。这会导致测试清理时仍残留其它 owner 的可恢复正文,后续用例可能串台/flake。建议至少清掉“当前 owner 的 namespaced key + 旧的 base key(兼容历史/无 owner)”;若底层 localStorage 支持枚举,再按前缀批量清理会更稳。
export function __clearAllForTest(): void {
  map.clear();
  goalMap.clear();
  activeDataOwnerId = null;
  try {
    window.localStorage.removeItem(RECOVERY_STORAGE_KEY);
  } catch {
    // ignore
  }

@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.

审查通过。单一策略入口 collabEntryPolicy 消除了双判定漂移,sticky 路由和可恢复 handoff 正确处理了 relay 窗口期,device-link allowlist 新增的只读通道 fail-closed,119 条测试全过。无 P0/P1。

@MagicLizi
MagicLizi merged commit b7c0982 into main Jul 31, 2026
14 of 15 checks passed
@MagicLizi
MagicLizi deleted the xdt/thoughtful-ritchie branch July 31, 2026 15:45
@MagicLizi

Copy link
Copy Markdown
Contributor

合了。device-link 双端判定终于收进一个 policy,断线重连窗口的 sticky 路由和可恢复 handoff 解决了一个实打实的竞态——119 条覆盖。

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