Skip to content

feat(pi): 重启用 type-ahead(原生 Message Queue,supportsTypeAhead)+ 限流权威 predicate 收敛 - #710

Merged
deepcoldy merged 7 commits into
masterfrom
wt/botmux-pi-type-ahead-pi-message
Aug 3, 2026
Merged

feat(pi): 重启用 type-ahead(原生 Message Queue,supportsTypeAhead)+ 限流权威 predicate 收敛#710
deepcoldy merged 7 commits into
masterfrom
wt/botmux-pi-type-ahead-pi-message

Conversation

@deepcoldy

@deepcoldy deepcoldy commented Aug 3, 2026

Copy link
Copy Markdown
Owner

本 PR 已按 codex 两轮复审「降级」定稿(head 3fc9cd6e5)。本正文描述的是最终净 diff,不再是初版的 reliableTurnTerminal 方案。演进见「评审收敛过程」一节。

改了什么(最终净 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.tsdrainPiTranscript 现在对每个 terminal stopReason 都 emit assistant_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 更新断言(pi supportsTypeAhead=true、pi reliableTurnTerminal=undefined)+ 新增「限流权威=仅 Claude 家族(claudeDataDir)」的 describe 块。

为什么

撤回的真因(本次必须解决的核心)

type-ahead 安全的前提是:能把「忙碌期合并的多条输入」产生的那条最终回复,准确归属到对应 turn / 飞书卡片。b2c2ba67(2026-06-16) 曾开启,次日 b7dfa0c0(2026-06-17) 撤回——当时 Pi 只有屏幕 marker Working...,没有任何 turn 边界,忙碌期合并多条输入会错归属 / 串卡片。撤回后 13 天,PR #327(2026-06-30) 给 Pi 加了 per-session JSONL transcript bridge(pi-transcript.ts)——正是撤回时缺的能力。

修掉的真 bug

drainPiTranscript 只在 stopReason==='stop' 且非空 content 时 emit。→ error/aborted turn(空 final)完全不 emit → type-ahead 下 CodexBridgeQueue 的 collecting head 永不关闭 → 队列头 wedge。这正是当年会串卡片的机理。

stopReason → terminal 映射(读 pi-agent-core 源码 + pi 0.80.6 真机实测):

stopReason 处理 说明
toolUse 跳过(mid-turn) 模型在调工具
stop completed(默认) 恒 terminal。正常工具步是 toolUse 而非 stop(255/255 验证);带 toolCall 的 stop 是 custom-tool terminate:true 的收尾记录,也是真 terminal
length completed(默认) 命中 token 上限的截断答案;仅当无 toolCall 才 terminal(带 toolCall 时 Pi failToolCallsFromTruncatedMessage→terminate:false 继续 loop)
error failed / pi_turn_error provider 错误;硬 terminal(无视 content)
aborted ambiguous / pi_turn_aborted 用户 Esc;硬 terminal。ambiguousfailed:Esc 可能落在工具副作用已完成之后,保留「不知副作用是否发生」审计语义 + 允许同 generation 迟到 completed 结算(对齐 Codex/TraeX turn_aborted)。仍 !== completed,照常 drop pending turn 释放队列头

为什么reliableTurnTerminal(type-ahead 不依赖它)

type-ahead 只 gate 在 supportsTypeAheadinput-gate.ts);回复归属走 structured-bridge 名单(Pi 已在内)。reliableTurnTerminal更强的承诺——始终落盘的权威 turn 边界——而 Pi 给不了(0.80.6 真机验证):

  1. Pi 的 SessionManager 用短命 appendFileSync(open→append→close)写 JSONL,进程全程不持有 session fd/proc/<pid>/fd + lsof 整轮都查不到,6s 紧密轮询也抓不到瞬时 append fd)→ 基于 pid 的 session rotation 跟随不可用,durable 交付也无可靠边界。
  2. custom tool 返回 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 × 多后端横向架构)

  • structuredRateLimitAuthoritative gate 收敛(唯一跨 CLI 行为改动):结构化限流 emit(maybeEmitStructuredRateLimit)只在 Claude bridge(bridgeJsonlPath 路径)存在。原 gate reliableTurnTerminal 会把 codex/grok/traex 也纳入抑制,但它们的 error terminal 只映射成 receipt、不发 limited 状态 → 真 429 会丢 Dashboard「需要你」+ 退避。改 gate 于 claudeDataDir(真有结构化 emit 的 Claude 家族:claude-code/seed/relay/genius),一并修正 codex/grok/traex 长期的 latent 过度抑制(恢复屏幕 rate 扫描是安全方向;usage 类本就走屏幕不受影响)。⚠️ 这是共用路径行为改动,请 reviewer 确认扩大范围可接受。
  • VC 会议 delivery:Pi 仍不是 delivery consumer(gate on reliableTurnTerminal,Pi 未设)——与「Pi 无可靠落盘边界」一致,无回归。
  • terminalStatus/terminalErrorCode:喂进共用 CodexBridgeQueuecodex-bridge-queue.ts + bridge-fallback-gate.ts 消费),与 codex/grok/traex 同款,非死代码。
  • idle 检测:Pi 纯 quiescence(无 readyPattern/injectsReadyHook)。未开 reliableTurnTerminal,故 busy-marker idle 探针 + reattach 探针(gate on busyPattern)全部保持不变,reattach 持久 pane 仍靠 Working... 标 ready;assistant_final 落地时额外 fireIdle。
  • 共用 drain / 其它 20+ CLICodexBridgeQueuestructured-bridge-clis、worker 侧 bridge/flush 逻辑未改;worker 净改动仅限流 gate 一行。
  • 跨平台:drain 是纯文件读;findPiTranscriptByPid(attach 时路径解析用,非 rotation)已有 Linux /proc + macOS lsof 双路径,本 PR 未改其返回语义(初版加的 mtime-newest 已随 rotation 代码一并回退为首个匹配)。

评审收敛过程

  • 初版 86fd82a1d:加 supportsTypeAhead + reliableTurnTerminal
  • codex 一轮 → 1e81c87d4:修 3 项(stop 恒 terminal / session rotation 跟随 / 限流 gate 改 claudeDataDir),abortedambiguous
  • codex 二轮 → 本 head 3fc9cd6e5(降级,申晗拍板):真机核实 ①Pi 全程不持 session fd(rotation 跟随是死代码)②custom-terminate 无磁盘边界 → 去掉 reliableTurnTerminal,删掉本轮加的 rotation 跟随代码,只留 supportsTypeAhead。codex 已认可的 3 项(aborted→ambiguouslength+toolCall mid-turn、限流 gate 改 claudeDataDir)保留。

实测验证

  • pnpm build ✅ 绿(tsc exit 0,dashboard bundle 正常产出)。
  • 隔离跑(避免全套 mock-state 泄漏掩盖):
    • pi-transcript 11/11 + write-input 120/120 = 131/131
    • 受影响/相邻 CLU(cli-adapters 313 / structured-bridge-clis / codex-bridge-queue / bridge-fallback-gate / cli-usage-limit / vc-meeting-delivery-receiver / vc-meeting-consumer-profile-bootstrap / relay-adapter / seed-adapter / traex-transcript / initial-prompt-arg-limit)= 622/622
    • worker/bridge/vc 广测(worker-restart-race / worker-backend-exit-crash-order / worker-pipe-initial-screen-order / claude-transcript / bridge-turn-queue / dashboard-vc-consumer-profiles-api+ui / prompt-builder / command-handler / cost-calculator / fs-policy)= 636/636

手动验证建议(合并后 live)

type-ahead 的多卡片归属需飞书真机验证:给 Pi bot 发一条会跑较久的任务,趁忙碌再连发 1–2 条,确认每条落到各自独立卡片、最终回复不串卡、中断(若触发)不会让卡片卡在「工作中」。

## 背景
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 deepcoldy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

复审结论:当前建议修改后再合并。主路径(普通 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 86fd82a1d8807976c7bbed7b647987f3b0388f6cpnpm 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;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[P1] 这里把所有带 toolCall 的 stop/length 都视为 mid-turn,但 Pi 0.80.6 的公开扩展 API 允许 custom tool 返回 AgentToolResult.terminate: truepi-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 边界。

Comment thread src/adapters/cli/pi.ts
// 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,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[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 → 下一轮 回归测试。

Comment thread src/adapters/cli/pi.ts Outdated
// 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,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[P2] 这个 flag 还会令 structuredRateLimitAuthoritative() 对 Pi 返回 true,从而给 detectCliUsageLimitsuppressRateKind:true。但结构化限流事件目前只有 Claude 的 bridgeIngest → maybeEmitStructuredRateLimit() 发布;Pi drain 只把 error 映射成 failed terminal,没有任何 screen_update: limited/retryAt 信号。因此真实 429 会失去 Dashboard「需要你」和退避语义(durable delivery 还可能快速重试耗尽预算)。PR 描述中的“交给 transcript 权威”目前没有实现。请拆分 reliableTurnTerminalstructuredRateLimitAuthoritative 两个 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 deepcoldy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

delta 复审结论:aborted → ambiguouslength+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 核验。

Comment thread src/services/pi-transcript.ts Outdated
if (hit && !hits.some((seen) => seen.cliSessionId === hit.cliSessionId)) hits.push(hit);
}
return undefined;
return newestHit();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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 跑一次集成验证。

Comment thread src/services/pi-transcript.ts Outdated
// 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'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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 deepcoldy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

三轮 delta 复审:降级方向正确。去掉 Pi 的 reliableTurnTerminal 后,二轮两个 P1(不可用的 pid rotation、不可持久化的 custom terminate terminal)都不再构成 capability 违约;删除 rotation follower 也干净。aborted→ambiguouslength+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 均已绿。

Comment thread src/services/pi-transcript.ts Outdated
// 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'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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:stoplength 都只在 !hasToolCall(content) 时 terminal;真实 custom terminate 缺终态则按你已接受的 quiescence + 下一普通 user HOL-drop 降级。请删除对应 fabricated fixture,并同步改掉文件头/此处把 stop+toolCall 解释为 terminate 的注释。

Comment thread test/write-input.test.ts Outdated
// 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();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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 deepcoldy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

四轮 delta 复审:通过,无 blocker。

  • stop/length + toolCall 统一跳过,直到 tool-call-free terminal:修复正确;对应测试已翻转,custom terminate 作为 accepted gap 的降级语义一致。
  • isStructuredRateLimitAuthoritative(adapter) 已抽成纯函数,worker 确实委托该函数;正反 adapter 与 nullish case 的直接测试能锁住 claudeDataDir gate。
  • 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。
Comment thread src/adapters/cli/pi.ts
// 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,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[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 deepcoldy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

五轮独立 delta 复审(live head cc66dcb1):建议修改后再合并,重新打开 1 个 P1(见 inline)。当前账号与 PR 作者相同,无法提交 GitHub 的正式 Request changes,因此用 COMMENT 记录阻塞结论。

核心结论:

  • stop/length + toolCall 等待后续 tool-call-free terminal、error→failedaborted→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 隔离绿。
@deepcoldy deepcoldy changed the title feat(pi): 重启用 type-ahead(原生 Message Queue)+ 补全 transcript turn 边界 feat(pi): 重启用 type-ahead(原生 Message Queue,supportsTypeAhead)+ 限流权威 predicate 收敛 Aug 3, 2026
@deepcoldy
deepcoldy merged commit 3973058 into master Aug 3, 2026
6 checks passed
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚀 Released in v3.9.0

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.

1 participant