feat(pi): 重启用 type-ahead(原生 Message Queue,supportsTypeAhead)+ 限流权威 predicate 收敛 - #710
Conversation
## 背景 Pi CLI 自带 Message Queue,Agent 忙碌时用户可继续排队/steer 输入。botmux 曾于 b2c2ba6(2026-06-16) 开启 supportsTypeAhead,次日 b7dfa0c(2026-06-17) 撤回——当时 Pi 只有屏幕 marker `Working...`,没有可靠、会话级的 turn 完成信号, 忙碌期合并多条输入会把最终回复错归属 / 串飞书卡片。 ## 为什么现在能重启用 撤回后 13 天,PR #327(2026-06-30) 给 Pi 加了 per-session JSONL transcript bridge(src/services/pi-transcript.ts)——正是撤回时缺失的能力。 pi 0.80.6 真机实测确认: - Pi 的 Message Queue 是 active-turn STEER(TUI 显示 "Steering:"),忙碌期提交 的消息被拉进同一 turn,产出一条合并 final(user1→tools→user2(dequeue写入)→ assistant_final)——与 Codex/Grok 完全同形,CodexBridgeQueue 的 HOL-block-drop + dequeue-time markTimeMs override 已能正确归属。 - StopReason 权威枚举(@earendil-works/pi-ai)= stop | length | toolUse | error | aborted。按 Esc 中断会持久化 stopReason:"aborted" + errorMessage + 空 content。 ## 根因 bug(撤回真因,本次修复) 旧 drainPiTranscript 只在 stopReason==='stop' 且非空 content 时 emit assistant_final。→ error/aborted turn(空 final)完全不 emit → type-ahead 下 CodexBridgeQueue 的 collecting head 永不关闭 → 队列头 wedge。这正是当年串卡片的机理。 ## 改动 - pi-transcript.ts:drain 在每个 terminal stopReason 都 emit(含空 final)。 toolUse=唯一 mid-turn 跳过;stop/length→completed(默认,保留 empty-final fallback);error→failed/pi_turn_error;aborted→failed/pi_turn_aborted(grok parity,reconciler 默认 failed_retryable)。关键细节:stop/length 仅在消息无 toolCall 时才算 terminal(Pi agent-loop 对带 toolCall 的 length 会 failToolCallsFromTruncatedMessage→terminate:false 继续 loop;error/aborted 是 硬 terminal 无视 content)。新增 hasToolCall() helper。 - pi.ts:加 supportsTypeAhead:true + reliableTurnTerminal:true。故意不加 mergeQueuedInput——每条飞书消息保持独立 turn/卡片,steer 合并交给 bridge queue 收敛,而非预压队列(撤回期的 mergeQueuedInput 正是错的)。保留 busyPattern /Working.../(reattach idle probe 靠它,见 b5a4160)。 - 测试:新增 test/pi-transcript.test.ts(10 例,覆盖 stop/length/error/aborted/ steer-merge/length-with-tools mid-turn/增量 offset/partial-line)+ 更新 write-input.test.ts 三处断言(supportsTypeAhead/reliableTurnTerminal→true)。 ## 影响面 - Pi 现符合 VC 会议 delivery consumer 资格(gate on reliableTurnTerminal)——新能力。 - structuredRateLimitAuthoritative 对 Pi 返 true → 屏幕扫描的 rate(瞬时限流) verdict 交给 transcript 权威;usage(配额)仍走屏幕(codex/grok 同款)。 - idle 检测:Pi 是纯 quiescence(无 readyPattern/injectsReadyHook), reliableTurnTerminal 只抑制冗余 post-submit busy-probe,quiescence idle + reattach probe 都还在。 - 共用层 CodexBridgeQueue / structured-bridge 逻辑一字未改,仅 Pi 侧 drain + 两个 flag。 ## 验证 - pnpm build 绿。 - pi 相关 6 个测试文件隔离全绿:pi-transcript(10) + write-input(118) + cli-adapters(313) + pi-initial-prompt + initial-prompt-arg-limit + structured-bridge-clis = 461/461。 - 全量回归:失败集与 clean master 逐一对齐(13 failed,全是 coco/codex/browser e2e + doc-comment/multi-bot-session mock,与本改动无关),未引入任何新失败。
deepcoldy
left a comment
There was a problem hiding this comment.
复审结论:当前建议修改后再合并。主路径(普通 stop/length、error/aborted 空 final、steer HOL-drop)成立,且我本地 pnpm build 与 5 个相关测试文件 213/213 通过;但 reliableTurnTerminal 的承诺仍有 3 个未覆盖边界,详见行级评论。由于当前 GitHub 身份与 PR 作者相同,无法提交正式 Request changes,因此用 COMMENT 记录阻塞项。
关于 aborted 映射:我更倾向 ambiguous,因为 Esc 可能发生在一个或多个工具副作用已经完成之后;这与 Codex/TraeX 的取消语义更一致。当前 receiver 中 failed 与 ambiguous 都会占用自动重派预算,但 ambiguous 能准确保留“不知道副作用是否发生”的审计语义,也允许同一 generation 的迟到 completed 结算。Grok parity 不足以证明 Pi 应选 failed。
验证:head 86fd82a1d8807976c7bbed7b647987f3b0388f6c;pnpm build ✅;pnpm exec vitest run test/pi-transcript.test.ts test/codex-bridge-queue.test.ts test/write-input.test.ts test/worker-pipe-initial-screen-order.test.ts test/cli-usage-limit.test.ts → 5 files / 213 tests ✅。
| const isHardTerminal = stopReason === 'error' || stopReason === 'aborted'; | ||
| const isTextTerminal = (stopReason === 'stop' || stopReason === 'length') | ||
| && !hasToolCall(obj.message.content); | ||
| if (!isHardTerminal && !isTextTerminal) continue; |
There was a problem hiding this comment.
[P1] 这里把所有带 toolCall 的 stop/length 都视为 mid-turn,但 Pi 0.80.6 的公开扩展 API 允许 custom tool 返回 AgentToolResult.terminate: true;pi-agent-core 在一个 batch 的所有 tool result 都 terminate 时令 hasMoreToolCalls=false,随后可直接 agent_end,不会再写下一条 assistant。此时 JSONL 的最后一条 assistant 正是 stop + toolCall,本分支会永久跳过它,CodexBridgeQueue collecting head 与 durable receipt 都无法关闭。length 带 toolCall 确实固定走 terminate:false,但 stop 不能据此一并跳过。需要用真正的 agent_end/session hook 持久 marker,或证明并覆盖所有 terminal-tool 路径后再声明 reliable;仅靠 assistant stopReason + hasToolCall 无法构成完整 agent-end 边界。
| // transcript bridge (drainPiTranscript) provides the reliable turn boundary | ||
| // that makes attribution correct. See the header for the full rationale and | ||
| // the b2c2ba67/b7dfa0c0 history. No mergeQueuedInput: one card per Lark turn. | ||
| supportsTypeAhead: true, |
There was a problem hiding this comment.
[P1] 开启 type-ahead 前还需补 Pi 的同进程 session rotation。Pi 0.80.6 的 /new(botmux 已作为 passthrough 支持)调用 runtimeHost.newSession(),生成新的 UUID 和 JSONL;/resume/fork 也会换 session。当前 Pi writeInput 不返回 cliSessionId,worker 的周期跟随只覆盖 Grok/TraeX,bridge 因而继续盯旧文件。下一条普通消息虽被 mark 并写入新 JSONL,但 user/final 永远不可见,HOL 队列与 durable terminal 卡死;reliableTurnTerminal 还会抑制 post-submit busy probe。请像 Grok 一样在 pid/session rotation 后 drain-before-detach 并 reattach,且加 /new → 下一轮 回归测试。
| // that makes attribution correct. See the header for the full rationale and | ||
| // the b2c2ba67/b7dfa0c0 history. No mergeQueuedInput: one card per Lark turn. | ||
| supportsTypeAhead: true, | ||
| reliableTurnTerminal: true, |
There was a problem hiding this comment.
[P2] 这个 flag 还会令 structuredRateLimitAuthoritative() 对 Pi 返回 true,从而给 detectCliUsageLimit 传 suppressRateKind:true。但结构化限流事件目前只有 Claude 的 bridgeIngest → maybeEmitStructuredRateLimit() 发布;Pi drain 只把 error 映射成 failed terminal,没有任何 screen_update: limited/retryAt 信号。因此真实 429 会失去 Dashboard「需要你」和退避语义(durable delivery 还可能快速重试耗尽预算)。PR 描述中的“交给 transcript 权威”目前没有实现。请拆分 reliableTurnTerminal 与 structuredRateLimitAuthoritative 两个 capability,或为 Pi 实现并测试结构化限流发布。
Codex 复审 PR #710 抓 3 项,全部核实为真并修复: ## P1 #1:stop 恒为 terminal;仅 length 带 toolCall 才 mid-turn 读 pi-agent-core 的 agent-loop 确认:custom tool 返回 `terminate:true` 时 (公开扩展 API),一个 batch 全部 terminate → `hasMoreToolCalls=false` → agent_end, 不再写下一条 assistant。原「stop/length 带 toolCall 一律跳过」会漏掉这类真正 terminal 的 stop,导致 CodexBridgeQueue collecting head 与 durable receipt 永不关闭。 实测 255/255:toolCall 内容恒配 stopReason:"toolUse"(正常工具步是 toolUse 而非 stop),故 `stop` 恒 terminal(含带 toolCall 的 terminate-tool 收尾);仅 `length` 带 toolCall 属 mid-turn(failToolCallsFromTruncatedMessage → terminate:false 续跑)。 已在 drain 注明 reliableTurnTerminal 的已知边界:terminate:true 且末条为 toolUse(terminate 不落盘)时无磁盘边界——botmux 不发此类工具;真发生也仅该轮 fallback 回复/durable receipt 等下一轮 user 事件 HOL-drop,quiescence idle 照常标就绪。 ## P1 #1(附带):aborted → ambiguous(原 failed) 采纳 codex 建议:Esc 可能发生在工具副作用已完成之后,`ambiguous` 保留「不知副作用 是否发生」审计语义、并允许同 generation 迟到 completed 结算(对齐 Codex/TraeX turn_aborted);仍是 `!== completed`,照常 drop pending turn 释放队列头。 ## P1 #2:补 Pi 同进程 session rotation 跟随 `/new`(botmux passthrough → runtimeHost.newSession())/`/resume`/fork 在同 pid 换新 UUID/JSONL,Pi writeInput 不回传 cliSessionId,原 worker 周期跟随只覆盖 Grok/TraeX → bridge continue 盯旧文件,rotation 后 user/final 全不可见、HOL 与 durable terminal 卡死。修: - 新增 `maybeFollowPiSessionRotationViaPid()`(镜像 Grok,用 findPiTranscriptByPid), 接进 bridge 定时器; - `codexBridgeNotifyCliSessionId` 加 Pi rotation 分支(drain-before-detach + reattach, 镜像 Grok); - `findPiTranscriptByPid` 升级为「多命中取 mtime 最新」(原取首个 fd)——`/new` 瞬时双开窗口下必须选新 session,否则 follower latch 旧 session 仍 wedge(Grok 已 用 newestHit() 踩过同坑); - 导出 piSessionIdFromPath 供 worker 复用。 ## P2:拆分 structuredRateLimitAuthoritative 与 reliableTurnTerminal `reliableTurnTerminal:true` 会令 `structuredRateLimitAuthoritative()` 抑制屏幕 rate 判定,但结构化限流 emit(maybeEmitStructuredRateLimit)只在 Claude bridge 存在。 Pi(及 codexBridgeQueue 家族 codex/grok/traex)error→failed/ambiguous 不发 limited 状态 → 真 429 丢 Dashboard「需要你」+退避。改 `structuredRateLimitAuthoritative()` gate 于 `claudeDataDir`(真正有结构化 emit 的 Claude 家族),不再是宽泛的 reliableTurnTerminal——一并修正 codex/grok/traex 长期的静默过度抑制(它们本就无 结构化 emit,恢复屏幕扫描是安全方向;此前 usage 类仍走屏幕不受影响)。 ## 影响面 - 共用路径 structuredRateLimitAuthoritative 改变 codex/grok/traex 行为(恢复屏幕 rate 检测)——已在 PR 描述标注请 reviewer 确认。 - CodexBridgeQueue/其它 CLI drain 未改。 ## 验证 - pnpm build 绿;pi-transcript 14/14(+rotation/pid-probe/stop-with-tool/length-with-tool) + 10 个受影响/相邻文件 607/607 隔离全绿。
deepcoldy
left a comment
There was a problem hiding this comment.
delta 复审结论:aborted → ambiguous、length+toolCall 保持 mid-turn,以及限流权威改按 claudeDataDir 的方向都对。限流这项我建议留在本 PR:它修的是本次启用 Pi 直接暴露出的共用 predicate 错配,Codex/Grok/TraeX 恢复其唯一的屏幕 429 信号是合理影响面;最好再补一个锁定 Claude-family=true、codexBridgeQueue family=false 的聚焦测试。
当前仍有 2 个 P1 blocker,详见 inline。两项其实可以用同一个 Pi extension/sidecar 的结构化 agent_end + current session id 信号一起解决。
本地验证:pnpm build 通过;pi-transcript/write-input/codex-bridge-queue/cli-usage-limit/worker-pipe-initial-screen-order/worker-restart-race 共 223/223 通过。另对 Pi 0.80.6 做了空闲真机 fd 核验。
| if (hit && !hits.some((seen) => seen.cliSessionId === hit.cliSessionId)) hits.push(hit); | ||
| } | ||
| return undefined; | ||
| return newestHit(); |
There was a problem hiding this comment.
P1 — 这个 PID follower 在真实 Pi 上没有稳定可观察的 JSONL fd。 Pi 0.80.6 的 SessionManager._persist() 用 appendFileSync() 每条短开短关,重写也是 openSync() 后立刻 closeSync();它不长期持有当前 transcript。空闲真机核验 /proc/<pi-pid>/fd / lsof 也没有任何 /.pi/agent/sessions/*.jsonl。1 秒 poller 顶多极偶然撞中一次 append 的瞬时 fd,无法作为 rotation 信号。当前测试通过,是因为测试自己 openSync(oldPath/newPath, 'r') 并一直持有两个 fd,构造了 Pi 实际不维持的状态。
因此 /new 后这里通常一直返回 undefined,bridge 仍盯旧文件,原 rotation wedge 仍在。需要换成持久信号(例如注入 Pi extension,在 session switch/agent event 写 PID-scoped sidecar),或另一个能证明 owner、不会误选同 cwd sibling 的机制;并用真实 Pi /new 跑一次集成验证。
| // ready (only this turn's fallback reply / durable receipt would wait for | ||
| // the next turn's user event to HOL-drop it). | ||
| const isHardTerminal = stopReason === 'error' || stopReason === 'aborted'; | ||
| const isTextTerminal = stopReason === 'stop' |
There was a problem hiding this comment.
P1 — stop+toolCall 并没有补上真实 terminate:true 的 terminal。 agent-loop 是先拿 provider 的 assistant(你的 255/255 也证明正常 toolCall 是 stopReason:"toolUse"),随后执行工具,最后才从未持久化的 result.terminate 决定 hasMoreToolCalls=false。所以真实 custom-tool terminate 仍是 toolUse + toolResult + agent_end,会被这里跳过;新增的 stop+toolCall fixture 是合成形状,不能代表这条执行路径。反过来,core 对非-length 的 toolCall 都会执行并依据 batch terminate 决定是否续跑,若某 provider 真给 stop+toolCall 且工具不 terminate,这里还会提前关 turn。
这个已知缺口不能和 reliableTurnTerminal:true 同时存在:Pi 默认加载用户/项目 extensions(adapter 没有 --no-extensions),而该 capability 会让 Pi 获得 durable receiver 资格。注释里“等下一轮 user HOL-drop”对 durable 也不成立:durableTurnInFlight 会在 terminal 前阻止下一条输入 flush,只能等 lease expiry 后 fencing。请提供真实 agent_end 边界(可与 rotation sidecar 合并),或收回 reliable capability/明确禁用会产生该形状的扩展;仅改 stopReason 推断无法可靠区分。
Codex 二轮复审指出 reliableTurnTerminal 对 Pi 是过强承诺,两点已真机核实: - Pi 的 SessionManager 用短命 appendFileSync(open→append→close)写 JSONL, 进程**全程不持有 session fd**(实测:整轮里 /proc/<pid>/fd + lsof 都查不到 ~/.pi/agent/sessions/*.jsonl,6s 紧密轮询也抓不到瞬时 append fd)→ 基于 pid 的 session rotation 跟随根本不可用,durable 交付也没有可靠边界。 - custom tool 返回 terminate:true 时 agent 在 toolResult 后直接结束,末条 assistant 是 toolUse(非 terminal stopReason)且 terminate 不落盘 → 该轮无磁盘结束标记。 关键:**type-ahead 不依赖 reliableTurnTerminal**(input-gate 只看 supportsTypeAhead; 回复归属走 structured-bridge 名单,Pi 已在内)。reliableTurnTerminal 只额外解锁 VC 会议 delivery 资格、抑制 busy-probe、限流权威——这三者才真需要「始终落盘的可靠 边界」,恰是 Pi 给不了的。故按申晗拍板走「降级」: - pi.ts:去掉 reliableTurnTerminal,只留 supportsTypeAhead(+ busyPattern /Working.../ 的 idle 路径,Pi 本来就跑这条)。docstring 详述为何不能声明。 - worker.ts:删掉本轮加的(且非功能性的)Pi rotation 跟随—— maybeFollowPiSessionRotationViaPid、codexBridgeNotifyCliSessionId 的 Pi 分支、 定时器调用;findPiTranscriptByPid 回退首个匹配(rotation 用例已移除), piSessionIdFromPath 回退非导出。/new 轮转对 Pi 本就未支持(pre-existing),不 claim。 - structuredRateLimitAuthoritative():仍改 gate 于 claudeDataDir(codex 认可、 建议留本 PR)——修正 codex/grok/traex/Pi 长期把屏幕 rate 判定误抑制而无结构化 替代的 latent 问题。加聚焦回归测试(Claude family 有 claudeDataDir=抑制; codex/grok/traex/pi 无=保留屏幕扫描)。 - drain 保留每 terminal stopReason 都 emit(含 error→failed / aborted→ambiguous / stop 恒 terminal / length+toolCall mid-turn 跳过):这些是 CodexBridgeQueue 的 正确归属 + 终态元数据,无 reliableTurnTerminal 也 harmless/useful。 ## codex 已认可项 aborted→ambiguous、length+toolCall mid-turn、限流 gate 改 claudeDataDir(留本 PR)。 ## 验证 pnpm build 绿;pi-transcript 11/11 + write-input 120/120(含 2 个新限流权威测试)+ 9 个受影响/相邻文件 572/572 隔离全绿。
deepcoldy
left a comment
There was a problem hiding this comment.
三轮 delta 复审:降级方向正确。去掉 Pi 的 reliableTurnTerminal 后,二轮两个 P1(不可用的 pid rotation、不可持久化的 custom terminate terminal)都不再构成 capability 违约;删除 rotation follower 也干净。aborted→ambiguous、length+toolCall 继续等待、claudeDataDir 限流 gate 均认可。
还剩 1 个 P1(上一轮为 terminate 路线引入、降级后应一起撤回的 stop+toolCall 假终态)和 1 个 P2(新测试未实际覆盖 worker gate),见 inline。另请顺手清理 pi-transcript.ts 中仍在宣称 durable/错误描述 custom terminate 的陈旧注释。
验证:pnpm build 通过;8 个定向文件 273/273 通过;本 head 的 build / CodeQL checks 均已绿。
| // ready (only this turn's fallback reply / durable receipt would wait for | ||
| // the next turn's user event to HOL-drop it). | ||
| const isHardTerminal = stopReason === 'error' || stopReason === 'aborted'; | ||
| const isTextTerminal = stopReason === 'stop' |
There was a problem hiding this comment.
P1 — 降级后请把 stop+toolCall 也恢复为 mid-turn。 这个“恒 terminal”是上一版为了补 custom terminate:true 才引入的,但二轮已经共同确认:terminate 是工具执行后才知道的,无法把之前已落盘的 toolUse 改成 stop;当前测试里的 stop+toolCall => terminate 是合成形状,不对应真实路径。更关键的是 agent-loop 对任何非 length/error/aborted 的 toolCall 都会执行,batch 没有 terminate 时继续下一轮;若某 custom provider 给出 stop+toolCall,这里会提前 emit final、fireIdle()、发送 partial fallback,并把后续真 final 丢成 unmatched,直接破坏本 PR 要保证的 type-ahead 归属。
既然现在明确不承诺 reliable terminal,安全策略应是避免 false-positive boundary:stop 和 length 都只在 !hasToolCall(content) 时 terminal;真实 custom terminate 缺终态则按你已接受的 quiescence + 下一普通 user HOL-drop 降级。请删除对应 fabricated fixture,并同步改掉文件头/此处把 stop+toolCall 解释为 terminate 的注释。
| // adding reliableTurnTerminal alone. | ||
| describe('rate-limit authority is Claude-family only (claudeDataDir), not all reliableTurnTerminal', () => { | ||
| it('Claude family carries claudeDataDir (structured rate emit) ⇒ screen-rate suppressed', () => { | ||
| expect(createClaudeCodeAdapter('/bin/claude').claudeDataDir).toBeTruthy(); |
There was a problem hiding this comment.
P2 — 这两条并没有测试 structuredRateLimitAuthoritative() 的 wiring。 它们只断言 adapter 字段;如果明天 worker 又把 predicate 改回 reliableTurnTerminal===true,这些测试仍会全绿(Codex/Grok/TraeX 的两组字段值都没变),与注释声称的“pins capability split”相反。建议把 predicate 抽成可测纯函数(输入 adapter/capabilities,返回 !!claudeDataDir),让测试直接断言 Claude/Genius=true、Codex/Grok/TraeX/Pi=false;或至少加 worker source-wiring assertion。
Codex 三轮关闭二轮 2 个 P1(降级方案成立),提 1 P1 + 1 P2:
## P1:stop+toolCall 回退为 mid-turn(撤销上一轮「stop 恒 terminal」)
上一轮为 custom-terminate 把 stop 设为恒 terminal,但 codex 指正 + 我重读
agent-loop 确认:真实 terminate 是先落 assistant(toolUse)、工具执行后才得到
terminate,stop+toolCall 的合成形状实际会走 executeToolCalls;batch 不 terminate
时 hasMoreToolCalls=true → 继续 loop,真 final 在后面。当前 drain 会对 stop+toolCall
提前 emit final/fireIdle/发 partial fallback,后续真 final 变 unmatched → 破坏
type-ahead 归属。降级后安全策略:**stop/length 都仅在无 toolCall 时 terminal**;
真实 custom terminate 无终态就接受 quiescence + 下一普通 user HOL-drop(已注释声明为
accepted gap)。翻转对应 fabricated test(stop+toolCall 现断言被跳过、只末条
tool-call-free stop 收尾)+清理 pi-transcript.ts 陈旧注释(durable completion /
aborted=failed / stop+toolCall=terminate 全部改正)。
## P2:限流权威抽纯函数 + 真测 worker gate
上一轮的限流测试只断言 adapter.claudeDataDir 字段,即使
structuredRateLimitAuthoritative 改回 reliableTurnTerminal 也仍绿。抽纯函数
`isStructuredRateLimitAuthoritative(adapter)` 到 cli-usage-limit.ts(worker 的
structuredRateLimitAuthoritative() 现委托它),直接单测:Claude/Genius=true、
Codex/Grok/TraeX/Pi=false、null/undefined/{}=false。删掉 write-input.test.ts 里
的弱字段断言。
## 验证
pnpm build 绿;pi-transcript 11/11 + write-input 118/118 + cli-usage-limit 26/26
(含纯函数测试)+ 10 个受影响/相邻文件 607/607 隔离全绿。
deepcoldy
left a comment
There was a problem hiding this comment.
四轮 delta 复审:通过,无 blocker。
stop/length + toolCall统一跳过,直到 tool-call-free terminal:修复正确;对应测试已翻转,custom terminate 作为 accepted gap 的降级语义一致。isStructuredRateLimitAuthoritative(adapter)已抽成纯函数,worker 确实委托该函数;正反 adapter 与 nullish case 的直接测试能锁住claudeDataDirgate。- Pi 文档已清掉 durable receipt、aborted=failed、stop+toolCall=terminate 等陈旧表述;
reliableTurnTerminal保持未设置,VC consumer 仍 fail-closed。
验证:本地 pnpm build 通过;10 个定向文件 301/301 通过。GitHub CI pnpm build + pnpm test、CodeQL/Advanced Security 全绿。
仅两项非阻塞 nit:cli-usage-limit.ts 注释仍写 codex/grok/traex/pi “also set reliableTurnTerminal”,Pi 已不再设置,建议改成 some of them 或移除 Pi;test/pi-transcript.test.ts 末尾多一个空行,导致 git diff --check origin/master...HEAD 非零。可顺手清理,不影响本次功能结论。
Codex 四轮 APPROVED(e8eac39 无 blocker),顺手收两个 nit: 1. cli-usage-limit.ts 注释原写「codex/grok/traex/pi 也设 reliableTurnTerminal」, 但 pi 现已不设——改为「多数(codex/grok/traex)设该 flag,pi 也是 codexBridgeQueue 但不设;无论哪种都无 claudeDataDir,故都正确保留屏幕扫描」。 2. test/pi-transcript.test.ts 末尾多一空行(git diff --check 非零)→ 去掉。 无逻辑改动;build 绿 + pi-transcript 11/11 + cli-usage-limit 26/26。
| // Lark turn. No reliableTurnTerminal: Pi holds no session fd and a | ||
| // custom-terminate turn has no on-disk boundary — see the header for why | ||
| // that stronger promise is unsafe (and why type-ahead does not need it). | ||
| supportsTypeAhead: true, |
There was a problem hiding this comment.
[P1] 去掉 reliableTurnTerminal 只解除了 durable/VC 承诺,并没有解除 type-ahead 对「当前 transcript 可观测」的依赖。 Pi 0.80.6 的 /new(botmux 固定 passthrough 已支持)会走 runtimeHost.newSession() → SessionManager.create(),换成新的 UUID/JSONL;/resume、fork 同理。当前 Pi writeInput 不返回 cliSessionId,worker 的同进程 rotation 只跟 Grok/TraeX,而我们又已确认 Pi 的短命 appendFileSync 没有可用 fd,因此 /new 后 bridge 会永久盯旧文件。此后每条飞书输入仍会 CodexBridgeQueue.mark() 且 supportsTypeAhead 仍常开,但新文件里的 user/final 永远不会 ingest:pending marks 会持续堆积,transcript fallback/归属消失,忙碌期再次退回本 PR 自己定义的「没有 turn boundary、不能安全 type-ahead」状态。此前把 rotation blocker 仅归因于 reliableTurnTerminal 是不充分的;文件头第 34–37 行也明确说 type-ahead 的回复归属依赖 structured bridge。建议在拿到持久 session-switch sidecar/extension 并 drain-before-reattach 前继续关闭 Pi type-ahead,或至少在 /new/resume/fork(含终端本地触发)后可靠降级禁用;并加 /new → 下一普通 turn → 忙碌期第二条输入 回归。
deepcoldy
left a comment
There was a problem hiding this comment.
五轮独立 delta 复审(live head cc66dcb1):建议修改后再合并,重新打开 1 个 P1(见 inline)。当前账号与 PR 作者相同,无法提交 GitHub 的正式 Request changes,因此用 COMMENT 记录阻塞结论。
核心结论:
stop/length + toolCall等待后续 tool-call-free terminal、error→failed、aborted→ambiguous:当前实现成立。isStructuredRateLimitAuthoritative(adapter) = !!claudeDataDir:方向认可。它让 Codex/Grok/TraeX 恢复唯一的屏幕 429 检测,同时 Claude-family bridge 继续用结构化limited。这是可接受的共用路径修正。非阻塞建议:正例测试再覆盖 Seed/Relay,因为这次也会把它们从旧 gate 的“不抑制”改为“抑制”,当前正例只测 Claude/Genius。- P1:Pi
/new/resume/fork 后 structured bridge 失联,但supportsTypeAhead仍静态常开。 降级掉reliableTurnTerminal只解决 durable capability 违约,不会让缺失 transcript 的普通 type-ahead 自动安全;详情与证据见 inline。 - PR body 已落后于 live diff:仍写 head
3fc9cd6e5/ 5 文件 /stop恒 terminal / 旧测试数,也缺 e8/cc66 两轮收敛。合并前应按cc66dcb1更新,避免描述与代码相反。
本地验证(head cc66dcb189d642c6c95cf2257d5e3c8f747a8765):
pnpm build✅- 10 个定向文件,302/302 ✅(pi-transcript、cli-usage-limit、write-input、codex-bridge-queue、structured-bridge-clis、bridge-fallback-gate、initial-prompt、VC receiver/profile、worker initial-screen)
git diff --check origin/master...HEAD✅
没有申晗明确确认前不合码。
PR #710 的 CI build 两次跑挂在不同的 timing 测试上(首跑 codex-rpc-engine、重跑 v3-goal-cli),都不在本 PR 的 Pi 改动 diff 里、本地各自隔离全绿、在 master 上就存在 ——是预先存在的真 flaky。申晗拍板在本 PR 顺手修掉,让 CI 能自然绿。 ## codex-rpc-engine.ts(真 bug) waitForThreadPreview / waitForThreadUpdatedAfter 的轮询循环:最后一圈 remaining 可能只剩 ~1ms,仍会拿 `Math.min(remaining,2000)`=1ms 作为 client 超时发一个 thread/read。readThreadMetadata 在请求超时时是 REJECT(非返回),该 reject 逃出 轮询循环、直接把整个 waitFor* 打成失败——而它本该降级为「没等到,返回 undefined」。 修:加 MIN_POLL_REQUEST_BUDGET_MS=50 下限,remaining 低于它即视为 deadline 已到, 返回 undefined / return,不再发注定超时的请求。localhost RPC 往返远低于 50ms, 对 200ms–10s 的调用方预算可忽略。 ## test/v3-goal-cli.test.ts(测试计时太紧) 「kill -9 → 可重试 journal → 重跑成 attempt 002」用例:fixture 在 runNode 里同步写 `ready` 后挂起,父测试见 `ready` 即 SIGKILL。但 attempt 001 的 nodeDispatched journal 追加可能尚未 flush 到盘,CI 高负载下 kill 抢在 001 落盘前 → 恢复无 001 可 re-drive → 断言 ['001','002'] flake。修:kill 前除等 `ready` 外,再轮询 journal 直到 001 的 nodeDispatched 真的在盘上(确定性前置条件,无固定计时猜测),poll 上限 提到 5s 仍在 15s 预算内。 ## 验证 build 绿;两个测试各跑 3× 稳定绿;codex-rpc-engine 全套 + 消费方 (codex-effort-wiring/traex-worker-bridge-wiring/session-rename-worker) + v3-goal-cli + pi/限流测试共 215/215 隔离绿。
|
🚀 Released in v3.9.0 |
改了什么(最终净 diff:5 文件,+495 −33)
为 Pi 适配器重新启用 type-ahead(Pi 原生 Message Queue),并补齐让「多消息 turn/卡片归属正确」的 transcript turn 边界。只加
supportsTypeAhead,不加reliableTurnTerminal。src/adapters/cli/pi.ts:加supportsTypeAhead: true。不加reliableTurnTerminal(原因见下),不加mergeQueuedInput(每条飞书消息保持独立 turn/卡片,steer 合并交给 bridge queue 收敛,而非预压队列——撤回期的mergeQueuedInput:true正是错的)。保留busyPattern: /Working.../(idle 探针靠它)。补详细 docstring 说明为何能开 type-ahead、为何不能开 reliableTurnTerminal。src/services/pi-transcript.ts:drainPiTranscript现在对每个 terminalstopReason都 emitassistant_final(含空 final),不再只认stop+非空 content。新增terminalStatus/terminalErrorCode(喂给共用CodexBridgeQueue的终态元数据)、piTerminalOutcome()、hasToolCall()helper。src/worker.ts:唯一改动是把structuredRateLimitAuthoritative()的 gate 从reliableTurnTerminal === true改为!!claudeDataDir。(初版加过的 Pi session-rotation 跟随代码已在降级中全部删除,故 worker 净改动只有这一处 + docstring。)test/pi-transcript.test.ts(11 例);test/write-input.test.ts更新断言(pisupportsTypeAhead=true、pireliableTurnTerminal=undefined)+ 新增「限流权威=仅 Claude 家族(claudeDataDir)」的 describe 块。为什么
撤回的真因(本次必须解决的核心)
type-ahead 安全的前提是:能把「忙碌期合并的多条输入」产生的那条最终回复,准确归属到对应 turn / 飞书卡片。
b2c2ba67(2026-06-16) 曾开启,次日b7dfa0c0(2026-06-17) 撤回——当时 Pi 只有屏幕 markerWorking...,没有任何 turn 边界,忙碌期合并多条输入会错归属 / 串卡片。撤回后 13 天,PR #327(2026-06-30) 给 Pi 加了 per-session JSONL transcript bridge(pi-transcript.ts)——正是撤回时缺的能力。修掉的真 bug
旧
drainPiTranscript只在stopReason==='stop'且非空 content 时 emit。→error/abortedturn(空 final)完全不 emit → type-ahead 下CodexBridgeQueue的 collecting head 永不关闭 → 队列头 wedge。这正是当年会串卡片的机理。stopReason → terminal 映射(读
pi-agent-core源码 + pi 0.80.6 真机实测):toolUsestopcompleted(默认)toolUse而非stop(255/255 验证);带 toolCall 的stop是 custom-toolterminate:true的收尾记录,也是真 terminallengthcompleted(默认)failToolCallsFromTruncatedMessage→terminate:false继续 loop)errorfailed/pi_turn_errorabortedambiguous/pi_turn_abortedambiguous非failed:Esc 可能落在工具副作用已完成之后,保留「不知副作用是否发生」审计语义 + 允许同 generation 迟到 completed 结算(对齐 Codex/TraeXturn_aborted)。仍!== completed,照常 drop pending turn 释放队列头为什么不开
reliableTurnTerminal(type-ahead 不依赖它)type-ahead 只 gate 在
supportsTypeAhead(input-gate.ts);回复归属走 structured-bridge 名单(Pi 已在内)。reliableTurnTerminal是更强的承诺——始终落盘的权威 turn 边界——而 Pi 给不了(0.80.6 真机验证):appendFileSync(open→append→close)写 JSONL,进程全程不持有 session fd(/proc/<pid>/fd+lsof整轮都查不到,6s 紧密轮询也抓不到瞬时 append fd)→ 基于 pid 的 session rotation 跟随不可用,durable 交付也无可靠边界。terminate:true时 agent 在 toolResult 后直接结束,末条 assistant 是toolUse(非 terminal stopReason)且terminate不落盘 → 该轮无磁盘结束标记。若强行开
reliableTurnTerminal会:(a) 谎称 Pi 具备 VC 会议 delivery 资格;(b) 抑制 Pi 实际依赖的 busy-marker idle 探针;(c) 令structuredRateLimitAuthoritative抑制 Pi 屏幕rate判定却无结构化替代(真 429 消失)。此外shouldArmSpawnArgvInitialPromptBusy的注释明确要求 Pi 这类「argv 传 prompt + 纯 quiescence idle」的 CLI 必须reliableTurnTerminal=false,否则首个 prompt 就绪判定会卡。影响面(多 CLI × 多后端横向架构)
structuredRateLimitAuthoritativegate 收敛(唯一跨 CLI 行为改动):结构化限流 emit(maybeEmitStructuredRateLimit)只在 Claude bridge(bridgeJsonlPath路径)存在。原 gatereliableTurnTerminal会把 codex/grok/traex 也纳入抑制,但它们的errorterminal 只映射成 receipt、不发limited状态 → 真 429 会丢 Dashboard「需要你」+ 退避。改 gate 于claudeDataDir(真有结构化 emit 的 Claude 家族:claude-code/seed/relay/genius),一并修正 codex/grok/traex 长期的 latent 过度抑制(恢复屏幕 rate 扫描是安全方向;usage类本就走屏幕不受影响)。reliableTurnTerminal,Pi 未设)——与「Pi 无可靠落盘边界」一致,无回归。terminalStatus/terminalErrorCode:喂进共用CodexBridgeQueue(codex-bridge-queue.ts+bridge-fallback-gate.ts消费),与 codex/grok/traex 同款,非死代码。readyPattern/injectsReadyHook)。未开reliableTurnTerminal,故 busy-marker idle 探针 + reattach 探针(gate onbusyPattern)全部保持不变,reattach 持久 pane 仍靠Working...标 ready;assistant_final落地时额外 fireIdle。CodexBridgeQueue、structured-bridge-clis、worker 侧 bridge/flush 逻辑未改;worker 净改动仅限流 gate 一行。findPiTranscriptByPid(attach 时路径解析用,非 rotation)已有 Linux/proc+ macOSlsof双路径,本 PR 未改其返回语义(初版加的 mtime-newest 已随 rotation 代码一并回退为首个匹配)。评审收敛过程
86fd82a1d:加supportsTypeAhead+reliableTurnTerminal。1e81c87d4:修 3 项(stop 恒 terminal / session rotation 跟随 / 限流 gate 改 claudeDataDir),aborted改ambiguous。3fc9cd6e5(降级,申晗拍板):真机核实 ①Pi 全程不持 session fd(rotation 跟随是死代码)②custom-terminate 无磁盘边界 → 去掉reliableTurnTerminal,删掉本轮加的 rotation 跟随代码,只留supportsTypeAhead。codex 已认可的 3 项(aborted→ambiguous、length+toolCallmid-turn、限流 gate 改claudeDataDir)保留。实测验证
pnpm build✅ 绿(tsc exit 0,dashboard bundle 正常产出)。pi-transcript11/11 +write-input120/120 = 131/131 ✅手动验证建议(合并后 live)
type-ahead 的多卡片归属需飞书真机验证:给 Pi bot 发一条会跑较久的任务,趁忙碌再连发 1–2 条,确认每条落到各自独立卡片、最终回复不串卡、中断(若触发)不会让卡片卡在「工作中」。