test(desktop): allow CRLF in voice recording gate assertion - #1440
Merged
MagicLizi merged 1 commit intoAug 3, 2026
Merged
Conversation
Signed-off-by: xxxcc <xxxcc123@126.com>
|
| Filename | Overview |
|---|---|
| apps/desktop/src/renderer/voice-input/tests/VoiceInputSection.recordingGate.test.ts | 两条 source-contract 正则均使用 \r?\n 接受 LF 和 CRLF,同时保留原有依赖数组约束,未发现功能性问题。 |
Reviews (1): Last reviewed commit: "test(desktop): allow CRLF in voice recor..." | Re-trigger Greptile
Contributor
There was a problem hiding this comment.
Pull request overview
该 PR 修复了 Desktop Renderer 侧的 source-contract 单测在 Windows(CRLF checkout)环境下的误报:原断言正则只接受 LF,导致 Windows CI 读取 VoiceInputSection.tsx 源码时换行符不匹配而失败。
Changes:
- 将
startFnKeyCapture相关的两条正则断言从\n\n调整为同时接受LF/CRLF(使用\r?\n组合)。 - 保持测试意图不变:仍然断言 callback 依赖数组必须为空,避免录制中途因 identity 变化导致 effect 重跑。
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Contributor
|
⏸️ 维护者确认门 本 PR 命中 product 维护者确认(修改语音录制门控测试文件)。 等待任一维护者在本 PR 上 Approve 后自动放行。如需修改请 Request Changes,作者改完后重新 Approve 即可。 讨论 issue:#1444 |
zqchris
added a commit
that referenced
this pull request
Aug 3, 2026
Codex P2:pi edit 的 diff 只按 edits[] 解析,真实事件会拿到空 diff 与 +0 -0。
核对 pi v0.83.0 的 core/tools/edit.ts 后确认它有**两种**入参形态:声明 schema
是 { path, edits: [{oldText,newText}] },同时 LegacyEditToolInput 仍接受顶层
{ path, oldText, newText } 并由 normalizeEditInput 归一化。只认任一种都会让
另一种退化成空 diff,因此改为两种都认。
- maker-shared 新增共享归一化器 piEditReplacements:先取 edits[],再把顶层
oldText/newText(两侧都是字符串才认)作为最后一段追加,顺序与 pi 的
normalizeEditInput 一致;单段内只有一侧是字符串时另一侧按空串,纯增/纯删
不会被丢掉。
- AgentActionRow(diff lightbox)、diffStats(行内 +N -N)、payloadSummary
(mobile payload)三处消费端改为共用它,消除各自解析导致的漂移。
- 测试补齐真实形态:两种入参各自的 diff/统计/渲染断言,含顶层形态必须给出
非空 diff(断死不出现 "diffs":[])与真实 +1 -2。
Copilot:补 FILE_PATH_TOOLS 注释说明它不是「所有 kind='file' 描述符」的集合
—— pi 的 ls 归一化成 kind='file' 并渲染文件 chip,但刻意不入列(目标是目录,
开 lightbox 无意义),点击仍走命令类就地展开;新增工具按「点击后该看到什么」
判断是否入列。
同时合入最新 upstream/main(带上 #1440 的 VoiceInputSection CRLF 断言修复),
本 PR 的 Windows CI 红是该基线问题所致(issue #1448),非本 PR 引入。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
20 tasks
yan-xdt
pushed a commit
to yan-xdt/cindy
that referenced
this pull request
Aug 3, 2026
* fix(pi): 工具调用展示按 harness 归一化,不再只显示裸工具名
PI 内置工具名全小写(bash/read/edit/write/grep/find/ls)、文件参数字段为
path,而展示层的 describeToolUse 只认 Claude Code 大写名与 Codex 名,PI
的调用一律落进 generic 分支 —— 行级动词兜底成「调用」,参数位只剩裸工具
名,命令原文、文件名、搜索模式全部看不见。
把 PI 内置工具接进同一条展示产线:
- describeToolUse 补小写分支(bash→command 走 intent 解析,read/ls→file
read,edit/write→file edit/create,grep/find→search);不用无脑
toLowerCase 统一,因为 CC 的 Bash 带模型写的 description 而 PI 无此
字段,展示策略不同。
- 行级动词映射补小写条目,「调用」变回「运行 / 读取 / 编辑 / 搜索」。
- diff 统计与 diff lightbox 认 PI 的 edits[].oldText/newText 与
write.content,PI 编辑行恢复 +N -N 与 diff 预览。
- 展开详情、文件 chip 与 Read lightbox、最近一轮改动路径、mobile payload
diff 同步认小写名与 path 字段。
灵动岛(agent-island/toolDetail)与 mobile 消息投影本就由 describeToolUse
驱动,随本次修复一并受益。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — pi edit 两种入参形态归一化,澄清 FILE_PATH_TOOLS 边界
Codex P2:pi edit 的 diff 只按 edits[] 解析,真实事件会拿到空 diff 与 +0 -0。
核对 pi v0.83.0 的 core/tools/edit.ts 后确认它有**两种**入参形态:声明 schema
是 { path, edits: [{oldText,newText}] },同时 LegacyEditToolInput 仍接受顶层
{ path, oldText, newText } 并由 normalizeEditInput 归一化。只认任一种都会让
另一种退化成空 diff,因此改为两种都认。
- maker-shared 新增共享归一化器 piEditReplacements:先取 edits[],再把顶层
oldText/newText(两侧都是字符串才认)作为最后一段追加,顺序与 pi 的
normalizeEditInput 一致;单段内只有一侧是字符串时另一侧按空串,纯增/纯删
不会被丢掉。
- AgentActionRow(diff lightbox)、diffStats(行内 +N -N)、payloadSummary
(mobile payload)三处消费端改为共用它,消除各自解析导致的漂移。
- 测试补齐真实形态:两种入参各自的 diff/统计/渲染断言,含顶层形态必须给出
非空 diff(断死不出现 "diffs":[])与真实 +1 -2。
Copilot:补 FILE_PATH_TOOLS 注释说明它不是「所有 kind='file' 描述符」的集合
—— pi 的 ls 归一化成 kind='file' 并渲染文件 chip,但刻意不入列(目标是目录,
开 lightbox 无意义),点击仍走命令类就地展开;新增工具按「点击后该看到什么」
判断是否入列。
同时合入最新 upstream/main(带上 makecindy#1440 的 VoiceInputSection CRLF 断言修复),
本 PR 的 Windows CI 红是该基线问题所致(issue makecindy#1448),非本 PR 引入。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
---------
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
Co-authored-by: Chris <4436110+zqchris@users.noreply.github.com>
zqchris
added a commit
to zqchris/cindy
that referenced
this pull request
Aug 3, 2026
- 多 receiver 聚合(subagent-live-cards.ts):一次 V1 `spawnAgent` 返回多个
receiverThreadIds 时,此前为各子线程建独立计数器却用同一 taskId 发更新 →
后到的快照把先到的覆盖成更小值(token/toolUses 回退),任一 sibling 先收口
还会把整张卡误报完成。改为聚合状态挂在 taskId 上、按 thread 存分量:
* token 是各线程的**累计快照**,按线程覆盖后求和(不能相加历史值,否则重复计);
* toolUses 用卡级 item id 去重(item id 跨线程唯一);
* 状态取聚合:任一线程 running → running;全终态后 failed > stopped > completed,
失败不被 sibling 的 completed/stopped 掩盖。
- 乱序通知重放(tracker):子线程 thread/started 已建立 lineage、但父线程 spawn item
尚未登记时,通知此前被直接丢弃 → 首个工具调用 / 初始 token / 甚至终态永久缺失。
改为对未登记子线程缓冲(只缓冲会消费的 method,线程数与每线程条数双封顶),
在 noteSpawnItem 登记后重放并回传一帧聚合快照。
index.ts 把重放帧安排在 translateItemNotification **之后**发:translator 对 V2
spawn 会推一帧 status=running,先发会被它把已重放出的终态盖回去。
- 测试 +6 例:多 receiver 用量累加与 token 覆盖语义、sibling 先收口不提前完成、
failed 跨到达顺序仍胜出、早到通知重放(含早到终态)、无可重放时返回 null、
不缓冲不消费的 method。
CI:Windows 红项是 `VoiceInputSection.recordingGate.test.ts` 的语音用例,断言对组件
源码做行尾敏感的正则匹配(`\n {2}\}, \[\]\);\n\n`),Windows CRLF checkout 必红 ——
与本 PR 无关,上游 makecindy#1440(2ce6bcd)已修。本次合并最新 upstream/main 带入该修复。
Signed-off-by: Chris <zqchris@msn.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
zqchris
added a commit
that referenced
this pull request
Aug 3, 2026
review P1(安全,Codex + Greptile 各报一次):原实现把同目录资源的**预签名地址**回填进 页面,而渲染态保留 JavaScript —— 内联脚本能读 `img.src` 再以 no-cors 外发,第三方即可在 有效期内下载该被控端文件;`<img src=".env">` 这类引用会把同目录任意文件变成可外传的 bearer URL。提示注入过的 agent 产物足以触发。两层修: 1. 资源不再以签名地址进页面:app 侧下载后转成 `data:` URI 再回填 (downloadRemoteMediaAsDataUri),页面里**不出现任何 bearer 凭证**;单资源 2 MiB 上限, 超限保留原引用。MIME 按扩展名白名单给准,表外类型不改写(给错会让样式表/脚本被 浏览器拒收、静默失效)。音视频刻意不在表内 —— 整份内联会撑爆内存。 2. 新增 htmlPreviewCsp:注入 `default-src 'none'` + 只放行 `data:` 资源 + `connect-src 'none'` 的 CSP,把页面网络出口交给渲染引擎强制封锁,"读到了也送不出去" 不再靠我们约定。CSP 挂在渲染载体(HtmlFileReader)上,任何进 WebView 的 HTML 都带策略。 代价已在 PR 写明:公网 https 图片/字体在预览里不再加载 —— 放行 img-src https: 等于留 `new Image().src='…?d=…'` 这条经典外传通道,会让整条封锁形同虚设。 review P2(SSH):资源取件的 `xdt-file://` 缺 sessionId/remoteHostId/workdir,被控端 parseSshMediaOrigin 会把远端 absPath 当本机路径交给 realpath —— SSH 会话下所有同目录资源 取件失败,被控桌面恰有同名路径时还会读到错误来源。remoteFileMediaUrl 与 fetchRemoteAbsFileToUrl 补可选 SSH 上下文(三项必须齐备,与被控端完整性校验同口径), 并**进取件缓存键**:同一路径在不同远端主机上是不同文件。 CI 红为既有基线问题,不在本 PR 修:Windows unit tests 失败用例是 src/renderer/voice-input/__tests__/VoiceInputSection.recordingGate.test.ts,已由 issue Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com> #1448 记录「在 main 上稳定失败(疑与 CRLF 检出有关)」,另有修复 PR #1440 + 维护者确认 #1444。本 PR 只改 apps/mobile,不可能影响桌面 renderer 的该用例;本地全绿。
20 tasks
This was referenced Aug 3, 2026
yan-xdt
added a commit
to yan-xdt/cindy
that referenced
this pull request
Aug 3, 2026
撤销 a23ef3f(为解锁 CI 误判的 recordingGate 兼容修复)后,该文件回到旧 LF 版, 但上游已由 makecindy#1440 合入 CRLF 修复(2ce6bcd)。同步到上游版本,PR 内该文件零 diff。 Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: yan <yan233@xd.com>
GaoWeiLiuXD
pushed a commit
to GaoWeiLiuXD/cindy
that referenced
this pull request
Aug 3, 2026
* feat(codex): 子代理卡补齐实时状态,与 Claude 子代理卡形态统一
Codex 子代理此前在 UI 上只有一张即时收口的「已启动」卡:没有运行中状态、
没有 tokens / 工具调用数 / 耗时,而 Claude 子代理卡靠 SDK task_progress
全程可见。差距的根因不在 UI —— 两者本就共用 AgentTaskCard —— 而在 Codex
侧缺数据源:子线程的 item / tokenUsage / turn 通知其实已经到达 Cindy 进程,
被 AppServerHost 的路由丢掉了。
- host: routeNotification 按 lineage 认出已知子线程,投到新增的
descendantNotification 通道。刻意不复用主线程 dispatch —— 主线程 handler
带 turn 级簿记(stale 判定、currentTurnId、status 推送、usageTracker 记账),
子线程事件灌进去会把子代理的 exec 渲染成主会话自己的工具调用并污染主 turn
用量。thread/started 仍只走专用的 descendantThreadStarted,不双通道下发;
血缘未知的线程保持原 TTL 缓冲语义(解 subscribe 竞争)。
- subagent-live-cards: 新增纯聚合模块(Map 查 + 计数,零 IO,热路径友好),
把子线程通知汇成 tokens / 工具调用数 / 耗时 + 状态。跟踪条目有界(默认 64,
优先淘汰已收口条目);工具类 item 按 id 去重,只发 completed 的 item 也计入。
- translator: spawn 卡改为发 agent_task_update(running),tool_result 仍就地
收口不留悬空工具调用;抽出 readCodexSubagentSpawnRegistration 供 index 复用。
- V1/V2 双轨通用:V2(Sol/Terra)的 subAgentActivity 按 agentThreadId 登记,
V1(老模型与自定义接入模型)的 collabAgentToolCall spawn 按 receiverThreadIds
登记,同一条聚合链路服务两轨。
- 卡片文案:有 live update 时不再显示「Subagent X 已启动」——title 与运行状态
已表达同样信息,留着会让 codex 卡比 Claude 卡多一行冗余文案。判据下沉
maker-shared 的 buildAgentTaskCardModel,桌面与手机端共用;历史回放(无 live
update)仍保留该回执,否则只剩一条裸 agentPath。
不改 system prompt、不改注入配置、不新增 UI 组件。
Signed-off-by: Chris <zqchris@msn.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 多 receiver 共享聚合 + 乱序通知重放
- 多 receiver 聚合(subagent-live-cards.ts):一次 V1 `spawnAgent` 返回多个
receiverThreadIds 时,此前为各子线程建独立计数器却用同一 taskId 发更新 →
后到的快照把先到的覆盖成更小值(token/toolUses 回退),任一 sibling 先收口
还会把整张卡误报完成。改为聚合状态挂在 taskId 上、按 thread 存分量:
* token 是各线程的**累计快照**,按线程覆盖后求和(不能相加历史值,否则重复计);
* toolUses 用卡级 item id 去重(item id 跨线程唯一);
* 状态取聚合:任一线程 running → running;全终态后 failed > stopped > completed,
失败不被 sibling 的 completed/stopped 掩盖。
- 乱序通知重放(tracker):子线程 thread/started 已建立 lineage、但父线程 spawn item
尚未登记时,通知此前被直接丢弃 → 首个工具调用 / 初始 token / 甚至终态永久缺失。
改为对未登记子线程缓冲(只缓冲会消费的 method,线程数与每线程条数双封顶),
在 noteSpawnItem 登记后重放并回传一帧聚合快照。
index.ts 把重放帧安排在 translateItemNotification **之后**发:translator 对 V2
spawn 会推一帧 status=running,先发会被它把已重放出的终态盖回去。
- 测试 +6 例:多 receiver 用量累加与 token 覆盖语义、sibling 先收口不提前完成、
failed 跨到达顺序仍胜出、早到通知重放(含早到终态)、无可重放时返回 null、
不缓冲不消费的 method。
CI:Windows 红项是 `VoiceInputSection.recordingGate.test.ts` 的语音用例,断言对组件
源码做行尾敏感的正则匹配(`\n {2}\}, \[\]\);\n\n`),Windows CRLF checkout 必红 ——
与本 PR 无关,上游 makecindy#1440(2ce6bcd)已修。本次合并最新 upstream/main 带入该修复。
Signed-off-by: Chris <zqchris@msn.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 嵌套子代理按血缘归属,订阅前缓冲的子线程通知按序补投
1. 嵌套子代理归属(greptile P1):孙线程的 spawn item 只出现在**子线程自己**的事件流里,
主线程的 itemStarted 钩子永远看不到 → noteSpawnItem 不可能登记孙线程,其通知全部落进
pending 且再无登记路径可重放,工具调用/token/终态一概不计入卡片。
改为利用 host 已有的 `descendantThreadStarted`(对**每一代**都触发并给出 parentThreadId):
新增 `noteDescendantThread(child, parent)`,父线程已归属某张卡时把子线程并入同一张卡并
重放其早到缓冲;父线程与子代理无关时无副作用。任意深度自然成立(子→孙→曾孙)。
2. 订阅前缓冲重放(codex P2):`thread/started` 与该子线程的 item/usage/turn 都早于 root 的
subscribeThread 到达时,它们缓存在**child id** 下;root 侧 drain 只排空 root id 的队列,
`replayBufferedDescendantThreadStarts` 又只重建血缘 → 早期工具数与 token 永久丢失,
漏掉 turn/completed 还会让卡片一直停在 running。
`routeDescendantThreadStarted` 在血缘建立后调用新增的
`drainBufferedDescendantNotifications`,按到达顺序把该 child 的非 start 通知补投进
descendant 通道(先删再投,避免后续血缘重建重复投递)。
同时把 `replayBufferedDescendantThreadStarts` 改为**先快照候选再处理** —— 补投会从
this.buffered 删除条目,边迭代边删同一个 Map 容易漏项。
测试 +4 例:
- 聚合器:嵌套(子→孙→曾孙)工具数累加与 token 按线程求和、全部代际终态后才收口且 failed
胜出;孙线程通知先到后经血缘重放补回(含早到终态,卡片不再停在 running);父线程与子代理
无关时零副作用。
- host:订阅前到达的 child 三条业务通知(item/usage/turn)与 grandchild 通知按原顺序补投、
thread/started 不进该通道、主线程 handler 零调用、同一批不被重复补投。
注:本轮首次全量单测遇到 vitest worker SIGSEGV(被 reporter 归类为 TEST_ASSERTION_FAILED),
重跑 EXIT=0 全绿且无 SIGSEGV —— 属工具链偶发崩溃,与本改动无关。
Signed-off-by: Chris <zqchris@msn.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — spawn 自身失败时不再被聚合快照盖回 running
`readCodexSubagentSpawnRegistration` 新增 `failed` 标记(V1 `collabAgentToolCall.status
=== 'failed'`;V2 的 subAgentActivity 无 status,恒 undefined)。聚合器据此:
- spawn 本身收口为失败时**不补发**聚合快照 —— translator 已推过 failed 帧,而那时子线程
还标着 running,补一帧就会把真实失败态盖回运行中(review);
- 同时把该 spawn 的线程直接标成 failed,后续迟到的子线程通知也不会把卡片翻回 running;
- 成功收口仍照旧重发聚合快照(否则会被 translator 合成的 completed 盖掉,上一轮已修)。
测试 +2 例:失败收口不发帧且迟到通知仍为 failed;成功收口仍重新声明真实聚合(running)。
CI 红项(Windows unit tests (1/2))经核对为**基线抖动,非本分支引入**,证据:
- 失败用例是 `packages/lizi-mcps/src/__tests__/contactsTools.test.ts`「系统回写: 锚点每批
立即回填…」,报错是 `Test timed out in 5000ms`(超时,非断言);
- 本分支对 `packages/lizi-mcps` **零改动**(`git diff upstream/main...HEAD -- packages/lizi-mcps`
为空),该测试文件最后一次改动是 7-24 的首次开源提交;
- 本地全量 `pnpm test:unit` EXIT=0 / 0 FAIL,该套件本地通过;
- 同一时段 upstream/main 自身的 client-ci 也是 failure,但红在**另一个**用例
(`device-link/__tests__/dispatchWeakNetwork.test.ts`,来自刚合并的 makecindy#1418)—— 说明当时
Windows runner 整体不稳。
规模自审(漂移门禁 [2] WARN +70%):以 merge-base 计,本 PR 共 11 文件 / +1391 −32,其中
**743 行是测试**(53%)。源码增长全部落在同一机制上:聚合器新文件(388)、host 血缘路由与
缓冲重放(+122)、index 接线(+73)、translator 登记判据(+81),无新增无关功能面。本轮
unresolved 归零、不再加功能面,属收尾阶段。
Signed-off-by: Chris <zqchris@msn.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 缓存未归属血缘并递归补绑,失败 spawn 上终态闩
两条 P1 都是乱序通知下的归属/终态问题,根因同一处:归属与重放逻辑分散在两个 note*
入口各写一遍,谁也没覆盖"父线程还没归属"这个中间态。这次抽出单一递归补绑路径统一。
**1. 未归属血缘不再丢弃(subagent-live-cards.ts:336)**
子线程的 thread/started 可能早于根线程的 spawn item 到达,而它在归属前就可能已派出孙
线程 —— 那条「孙 → 子」血缘此刻无从判断归属,原实现直接 return null 丢掉。后果:之后
noteSpawnItem 只绑直接子线程,孙线程已缓冲的工具/token/终态再无重放机会,卡片漏计,还
可能在孙线程仍在跑时提前显示完成。而这条边是孙线程**唯一**的入卡途径 —— 孙线程的 spawn
item 只出现在子线程自己的事件流里,主线程的 itemStarted 钩子永远看不到。
改为缓冲 `父 → 子` 边(pendingLineage,父线程数 × 每父子线程数双封顶),父线程一归属就
递归补绑整条链并重放各代的缓冲通知。递归带 visited 防环——血缘理论上是树,但通知来自
外部进程,不能假定。
顺带修正一个发帧条件:新线程并入会改变聚合状态(已显示完成的卡因孙线程仍在跑必须回到
running),所以不能只在"有内容被重放"时发帧,还要看聚合状态是否因此改变。
**2. 失败 spawn 的终态闩(subagent-live-cards.ts:247)**
上一轮我只把「当下已知的线程」标成 failed,这挡不住:applyNotification 的 turn/started
会无条件写回 running、turn/completed 会写 completed,迟到的 turn 生命周期通知照样能把卡
片从失败翻回运行中/已完成,覆盖 translator 的失败帧。这是我自己引入的洞,bot 指出得对。
改为卡上的 spawnFailed 闩,aggregateStatus 里优先于一切子线程状态返回 failed;失败时若卡
尚未建立也建卡上闩,这样失败先到、子线程后到的乱序同样锁得住,新并入的线程直接算 failed。
闩上之后仍继续吸收 token / 工具计数 —— 派发失败但子线程已烧掉的量该算进去,只是状态恒为
failed。
**验证**
- 新增 7 例回归测试(乱序血缘、多代链递归补绑、环、失败先到/后到、闩上仍计量、
已完成卡因迟到后代回到 running、缓冲有界)。
- 做了变异验证:把终态闩与血缘缓冲两行分别停掉,新增用例中 5 例立刻失败、其余 27 例照旧
通过 —— 确认这些测试真的守着这两个机制,不是写了个恒真断言。
- packages/maker-core codex 全套 507 passed;pnpm test:unit EXIT=0 / 0 FAIL;
maker-shared 与 desktop typecheck 通过,maker-core 无 typecheck script。
顺带合入 upstream/main:其中 eae5a49 修掉了此前把本 PR 三个 check 卡红的 device-link
outbox TTL 断言(makecindy#1477 与 makecindy#1418 同日合入撞出的语义冲突,在 main 上稳定失败,已关 makecindy#1501)。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 血缘重扫、去重登记留到淘汰、失败 spawn 补发已重放用量
三条都是子代理卡在**乱序/迟到通知**下丢数据,按根因分两处修。
**1. 逆序孙线程的血缘不再烂在缓冲区(host.ts)**
root 已订阅、而孙线程的 thread/started 先于父线程到达时,孙的血缘无从判断 → 连同它的业务
通知一起缓存在孙自己的 id 下。父线程随后建立血缘时,原实现只排空父线程那一条队列(而且按
契约跳过 thread/started),不再扫待解析的后代血缘 —— 孙线程的 tool / token / 终态通知会一直
留到过期,卡片漏计并可能持续显示运行中或提前完成。
改为在 routeDescendantThreadStarted 末尾复用 root 订阅时那套迭代重建。两者会互相调用,加
replayingDescendantLineage 重入闸:嵌套调用直接返回,由最外层那次的 for(;;) 跑完 —— 否则深
血缘下退化成 O(深度²) 的重复扫描。
**2. 工具 item 去重登记留到卡片淘汰(subagent-live-cards.ts)**
app-server 允许 turn/completed 先发、后台收尾的 item/completed 随后才到(codex/index.ts 的
终态墓碑注释写明了这个顺序)。原来 snapshot() 一收口就清 countedItemIds,那条迟到的
completed 会被当成新工具再加一次 → 卡片最终工具数虚高。
改为跟卡同生命周期(dropCard 时随卡释放),只按条数封顶(4096,按插入序淘汰最老的)防长跑
子代理无界增长 —— 被淘汰的 id 只在"几千个工具调用之前那条的 completed 现在才到"时才会重复
计数,现实中不发生。
**3. 失败 spawn 补发已重放的用量(subagent-live-cards.ts)**
V1 spawn 的 started phase 缺失/晚到时,子线程的工具与 token 先进缓冲、由 attachThread 重放
出来;若此后再无通知,原来无条件 return null 就意味着这些用量永远不显示 —— 而 translator 的
failed 帧本身不带 usage。上一轮加的 spawnFailed 闩让 snapshot() 恒为 failed,现在补发这一帧
不会重现"把失败盖回运行中"的老问题。没有可重放内容时仍然不发帧。
**验证**
- 新增 5 例回归测试:host 侧「已订阅 + 孙先到」与三代逆序链各一例,tracker 侧收口后迟到
completed 不重复计数、失败 spawn 有/无重放内容两条路径。
- 变异验证:把三处修复分别停掉(血缘重扫、终态清 id、失败分支 return null),对应 4 例立刻
失败、其余 43 例照旧通过 —— 确认这些测试真的守着机制,不是恒真断言。
- codex 全套 512 passed;pnpm test:unit EXIT=0 / 0 FAIL;maker-shared 与 desktop typecheck
通过,maker-core 无 typecheck script(该包存量类型错误在 claude-code 测试文件里,与本 PR
无关;已单独确认 agents/pi 与 agents/codex 下无新增错误)。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 对真 codex 二进制核对协议契约,补计 sleep 工具 item
review 指出的根因成立:这条热路径重新路由后代线程通知,而此前**全部**证据都是我手工构造的
事件 —— 手构造的问题是"我没想到的形状我也构造不出来",漏计类缺陷天然测不到。
用真 codex 0.145.0 二进制做了实测核对(方法与完整结论写进 PR 说明),直接查出一个真缺陷:
**TOOL_ITEM_TYPES 漏了 `sleep`。** `codex app-server generate-json-schema` 导出的 ThreadItem
共 18 个变体,`sleep` 的 schema 描述是 "Display item emitted by the interruptible `clock.sleep`
tool" —— 它是工具调用,漏掉会少计一次。已补,并加了一例把 9 个工具类型逐一计数、9 个非工具
类型逐一断言不计数的回归测试(此前只覆盖 commandExecution 一种)。
同时核对通过、说明既有假设站得住的部分(细节见 PR):
- `Thread.parentThreadId` 的 schema 描述:"only be set if this thread is a subagent" ——
正是 host 侧血缘路由的判据本身;root 线程实测 parentThreadId=null。
- 四个枚举与代码分支逐一吻合:TurnStatus(completed/interrupted/failed/inProgress)、
SubAgentActivityKind(started/interacted/interrupted)、CollabAgentTool(spawnAgent 等 5 个)、
CollabAgentToolCallStatus(inProgress/completed/failed)。
- `collabAgentToolCall.receiverThreadIds` 的描述确认"一次 spawn 对应新建 agent"的多 receiver
语义 —— 即卡片按 taskId 聚合、多线程分量累计的前提。
- 真实 turn 的通知顺序实测:turn/started → item/* → **thread/tokenUsage/updated** →
turn/completed。token 先于终态到达,这正是聚合快照不会带着过期 token 收口的前提。
- V1/V2 仲裁实测:0.145 下 `[features].collab` 已废弃(提示改用 `multi_agent`);自定义
provider 暴露的是 `multi_agent_v1`(子工具 spawn_agent / send_input / wait_agent /
resume_agent / close_agent),与 V2 只在特定 OpenAI 模型上启用的判断一致。
**未能实测的部分,如实记录**:没能真正 spawn 出一个子线程来端到端验证血缘形状与累计 usage
聚合 —— 需要让假 provider 按 responses API 的 SSE 分片格式伪造一次 spawn_agent 工具调用,
codex 不接受我构造的 output 形状,再往下是协议逆向工程,超出本 PR 应付的成本。PR 说明里写明
了这一面未验证及可执行的验证步骤。
验证:codex 全套 525 passed;pnpm test:unit EXIT=0 / 0 FAIL;maker-shared typecheck 通过;
desktop typecheck 除基线 makecindy#1543 的 7 条外无本分支错误。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — item/updated 也登记 spawn 映射;真 spawn 实测推进但未打通
**1. item/updated 补上 spawn 映射登记(真缺陷,已修)**
V1 的 spawn 是长跑 item(started → updated* → completed)。原来只在 started 与 completed 两帧
调 noteSubagentSpawnItem —— started 那帧若没到我们手里(turn 缓冲、stale turn 丢弃、上游省略),
映射要一直等到 completed 才建立。期间子线程的 item / token / turn 终态全被缓冲,卡片在整个运行
期(可能好几分钟)没有实时数据,最后才一次性补上 —— 那恰好是本 PR 要解决的问题本身(review)。
改为三个生命周期阶段顺序一致:先登记 → 翻译 → 后发重放帧(重放帧必须后发,否则会被 translator
刚推的 running 帧盖回去)。
回归测试用 index.test.ts 既有的 fake host 驱动**真实 handler**(不是结构断言):只发 updated、
刻意跳过 started,断言子线程随后的 item/token 当场路由成卡片更新(toolUses=1、totalTokens=4242、
status=running),而不是被缓冲。变异验证:去掉这次登记,该例立刻失败(usage 为 undefined)。
**2. 真实子线程 spawn 实测:推进明显,但仍未打通(如实记录)**
上一轮卡在"codex 不接受我构造的 assistant output"。这轮定位到根因并解决了那一层:responses API
必须走 `response.output_item.added` / `.function_call_arguments.delta` / `.done` /
`response.output_item.done` 分片,**只把 output 塞进 `response.completed` 会被静默忽略**。改对之后
codex 确实解析并路由了工具调用 —— 证据是 router 开始报明确错误而不是无反应。
新的卡点是 codex 对 **namespace 工具**的分派名:`multi_agent_v1` 是 `type: "namespace"`,内含
`spawn_agent` 等 5 个子工具。逐个试过 `spawn_agent` / `multi_agent_v1.spawn_agent` /
`multi_agent_v1__spawn_agent` / `_` / `-` / `:` 六种写法,全部被 router 判为
`unsupported call` —— 分派约定不在导出的 JSON Schema 里,继续试属于对闭源二进制做协议逆向。
因此「真实 spawn 出子线程」这一面**仍未验证**,PR 说明里写明了缺口与两条可执行的补验路径。
本线程留给放行人裁决是否以它为合并前置,我不自行 resolve。
验证:codex 全套 529 passed;pnpm test:unit EXIT=0 / 0 FAIL;maker-shared typecheck 通过;
desktop typecheck **0 error**(上游已修 makecindy#1543,基线红消失)。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 聚合器也消费 item/updated(与主线程 spawn 路径同因)
上一轮修的是**主线程 spawn item** 的 updated 登记;这条是同一个因在**子线程工具 item** 上的镜像:
聚合器的 applyNotification 只认 item/started 与 item/completed,CONSUMED_METHODS 也不缓冲
updated。于是长跑工具若首个可见阶段是 updated,在 completed 到达前不计入实时工具数;会话在
completed 前中断则**永久漏计**。
两处一起补:applyNotification 的 item 分支加 'item/updated',CONSUMED_METHODS 加同一项(登记前
到达时要能进缓冲、登记后重放)。去重仍靠 countedItemIds 的 item id,同一 item 的
started / updated / completed 只会计一次 —— 这也是加 updated 不会引入重复计数的原因。
既然上一轮已经接受"started 可能缺失"这个前提并据此修了主线程路径,同一前提在子线程侧也成立,
不该只修一半。
回归测试 2 例:首帧为 updated 时当场计数且同 item 后续 updated/completed 不重复计数;登记前的
updated 进缓冲、登记后被重放出来。
验证:codex 全套 531 passed;pnpm test:unit EXIT=0 / 0 FAIL;maker-shared typecheck 通过;
desktop typecheck 0 error。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 断连时收口在跑的卡;卡片起点回溯到最早证据
**1. 断连时把仍在跑的卡收成终态(P1)**
tracker 只靠后代 `turn/completed` 写终态,而 transport error / 强制 retire / thread cleanup
failure 之后那些通知**永远不会再到**。原来两条收口路径只调 `subagentLiveCards.clear()` 丢内部
状态,不发任何帧 —— 桌面与手机的 task-update map 于是一直留着最后一帧 `running`:进程早就死了、
或者用户已经重连,卡还在原地转圈(review)。
新增 `drainRunningForShutdown()`:为每张非终态卡把 running 线程改成 `stopped` 再产出快照,由
`closeSessionHandle` 与 `terminateHandleAfterThreadCleanupFailure` 两处在 `clear()` **之前**发出。
两个细节是刻意的:① 报 `stopped` 而不是 `failed` —— 会话没了不代表子代理失败,它是被中断的;
② 改线程状态而不是在 snapshot 里特判,这样 aggregateStatus 自然收敛,也保住 `spawnFailed` 闩的
优先级(派发本身失败过的卡仍报 failed)。线程集合为空的卡(spawn 刚认出、子线程还没 started)
补一个占位线程,否则 aggregateStatus 对空集合返回 running,那张卡还是会转圈。
**2. 卡片起点回溯到最早证据(P1)**
`startedAt` 原来取建卡时刻。但 spawn 的 started/updated 阶段可能缺失或晚到,等 completed 才建卡
时子线程其实已经跑了一段,而那段时间的通知都躺在缓冲里(没有时间戳)—— 于是已消耗的时长整段漏掉,
长跑子代理甚至显示接近 0ms。
新增 `firstSeenAt`:第一条缓冲通知或第一条未归属血缘边就记下时刻,建卡时取本次 spawn 子线程里
最早的那个;没有任何早于建卡的证据时才退回 now()。
验证:新增 4 例(仍在跑的卡被收成 stopped 且已收口的不重复发、drain 幂等、线程未登记的卡也能收口、
起点回溯 60s、无早期证据时回退 now)。变异验证:把 startedAt 改回 now()、把 drain 的非终态判断短路,
对应 3 例立刻失败。codex 全套 535 passed;pnpm test:unit EXIT=0 / 0 FAIL;
maker-shared typecheck 通过;desktop typecheck 0 error。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — 子代理帧不参与主 turn 存活判定
`eventQueue.push` 上装了探针:每条事件都会刷新 `upstreamIdleLastEventAt` + `armUpstreamIdle()`,
并喂给 `observeReconnectStallEvent`。而 `agent_task_update` 就在 `isReconnectRecoveryEvent` 的
白名单里(第 4631 行)。于是子线程一有进展,就会重置主线程的静默计时并清掉 reconnect deadline ——
主 turn 其实已经哑火,却因为子代理还在跑而**永远检测不出来**(review)。子线程有进展 ≠ 主 turn 恢复。
刻意**不**从 `isReconnectRecoveryEvent` 白名单里删 `agent_task_update`:那对 Claude 主线程的 Task
更新是正确的语义,删了会误伤。修在源头 —— `emitSubagentCardUpdate` 期间置 `emittingDescendantUpdate`,
探针据此跳过存活判定(push 是同步的,set 与 clear 之间不会被别的事件穿插)。
回归测试用既有的 reconnect-stall harness:spawn 登记完成后发 reconnect 提示,此后**主线程一条事件
都不发**,只让子代理每 30s 产出一次工具调用,断言 ① 卡片确实在更新(证明子代理帧真的流过来了,
不是测试没生效)② deadline 照旧到点收口为 `codex_reconnect_stalled`。
写这条测试时先踩了一次自己的坑:第一版把 spawn 的 `itemStarted` 放在 reconnect 提示**之后**,
而那是合法的主线程进展、本身就会清 deadline —— 于是测的是"主线程事件清了 deadline",不是"子代理
帧不该清"。已调整顺序并在注释里写明,免得后来人照抄错版本。
档位说明:这改的是 upstream-idle / reconnect 存活状态机,按 owner 22:08 的分档属 **D 档**,
所以跑了全量,不是只跑单文件。
验证:pnpm test:unit EXIT=0 / 0 FAIL;codex/index.ts 自身无类型错误(该包既有 test 文件的存量
类型错误与本改动无关)。变异验证:把探针的 `!emittingDescendantUpdate` 判断短路,该用例立刻失败。
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
* fix: address review — transport 断连也收口在跑的子代理卡
app-server 崩溃 / stdio 断开走的是 `scope: 'transport'` 那条错误路径:订阅当场作废,
后代通知永远不会再到,而 tracker 的终态只由后代 turn/completed 写入。原来这条分支只
广播 error、清主线程缓存,子代理卡停在最后一帧 running —— 进程早死了,界面还在转圈。
复用 close() / cleanup-failure 已有的 drainRunningForShutdown() + clear(),不新增机制。
回归测试模拟一次普通断连:验证在跑的卡收到 stopped 终态且保住已聚合的 token/工具数,
重复断连与迟到的后代通知都不再投递新帧,close() 的同一套 drain 保持幂等。
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
---------
Signed-off-by: Chris <zqchris@msn.com>
Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com>
Signed-off-by: Chris <zqchris@users.noreply.github.com>
Co-authored-by: Chris <4436110+zqchris@users.noreply.github.com>
20 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
这次改了什么
摘要
修复 Windows CI 上
VoiceInputSection.recordingGate.test.ts的 source-contract 正则只接受 LF、拒绝 CRLF 的问题。Windows checkout 读取 renderer 源文件时使用 CRLF,导致该测试在 main 的 Windows 2/2 分片失败。变更类型
feat新功能fix缺陷修复refactor/perf重构或性能优化docs/test/chore文档、测试或工程维护范围
startFnKeyCapture的两条正则同时接受 LF 与 CRLF。UI 变化
怎么验证的
自动验证
手工验证
不涉及。GitHub Actions Windows unit-test 分片将验证 CRLF checkout 场景。
未执行的验证
未运行本地 unit test,按任务约束执行;依赖 CI 的 Windows 分片验证。
风险
风险分类
影响与回滚
提交前检查
git commit -s)