feat(trigger): botmux 驱动 codex-app 三能力(per-turn model/effort + usage + 结构化澄清 awaiting_input) - #638
feat(trigger): botmux 驱动 codex-app 三能力(per-turn model/effort + usage + 结构化澄清 awaiting_input)#638deepcoldy wants to merge 4 commits into
Conversation
对齐 riff B 模式(botmux 驱动 codex)能力,第一批(#1/#3,不依赖交互方向): - TriggerRequest.options 加 model?:string + reasoningEffort?:'low'|'medium'|'high'|'xhigh', 带校验(model ≤200 字符、effort 枚举)。 - 仅新建会话生效(fold-in 到已有 worker 不覆盖,符合文档语义):trigger-session 在首 fork 前把 override stamp 到 session,sessionAgentConfig 冻结所选 model、 init 携带 reasoningEffort → worker → codex buildArgs。 - codex 适配器注入 `-c model_reasoning_effort=<effort>`;'xhigh' 向下收敛到 codex 的 'high'(codex 无更高档)。 - TriggerResponse + AsyncStateInputs 加 usage:{inputTokens,outputTokens, cacheReadTokens,cacheCreateTokens}(字段名对齐 riff TaskTokenUsage)。 - 仅 completed 态发;wrapper 用 getSessionTokenUsage 从会话 transcript(CLI 无关) 读,best-effort——CLI 不支持/重启后 transcript 丢就省略该字段,不报错。 - 只在已有 completed 结果时才读 transcript,避免 running/failed 轮询的无谓 IO。 影响面:只走 async trigger 专用路径;model/effort 仅 codex/codex-app 消费,其它 CLI 忽略 reasoningEffort(buildArgs 不解构即丢弃)。校验/透传/usage 各加单测, build 绿。#2(awaiting_input + answer 通道)随后单独提交。 Co-Authored-By: Claude <noreply@anthropic.com>
…er 通道
对齐 riff B 模式(botmux 驱动 codex-app)的 clarification 能力——把 codex app-server
的结构化交互事件从"自动短路应答"改成透出 awaiting_input、由调用方程序化答复。
链路(runner → worker → daemon → 端点 → 代理):
- codex-app-runner:requestUserInput / elicitation 不再自动 answers:{}/cancel,改为
解析出 question/details/authChallenge、emit awaiting_input marker 并 hold 住请求;
收到 answer 输入行后按事件类型映回 codex-app respond(requestUserInput→answers 单
字段、elicitation→accept+content)。命令/补丁审批仍自动 approve(trusted,与 A 一致)。
多必填字段 schema 收敛到单文本 + 日志告警(真高频再立结构化表单)。
- runner-input:加 encodeRunnerAnswer + writeRunnerAnswer(复用同一 chunked 提交路径);
codex-app 适配器加 writeInteractionAnswer。
- worker:handleCodexAppMarker 认 awaiting_input → send({type:'awaiting_input'});
新增 answer_interaction 消息 → cliAdapter.writeInteractionAnswer 写回 runner。
- daemon:DaemonSession.awaitingInteraction 记录持有的交互;worker-pool 收 awaiting_input
存、收 final_output 清;resolver 加第 5 态 awaiting_input(优先于 running、completed 仍
胜出、closed 忽略陈旧 awaiting);trigger-result 带 interaction{interactionId,turnId,
kind,question,details?,authChallenge?}(turnId 按 riff 依赖补上)。
- 端点:POST /api/sessions/:id/answer body {interactionId, answer:{text}}(turnId/kind
调用方可带、daemon 只需 interactionId+text);校验 pending 匹配 → send answer_interaction;
dashboard 加代理(写 cookie 闸)。
契约与 riff 现有 BotmuxClient 实现逐字段对齐(BotmuxInteraction / answer body)。
新增 awaiting_input 解析单测(优先级/仅 live/authChallenge 透传);async 相关 73 测全绿;
trigger-session/webhook 回归 42 测绿;build 绿。
影响面:仅 codex-app 结构化运行路径 + async trigger 专用分支;不碰纯 codex(PTY)、
codex-rpc(traex)、claude、普通会话。命令审批短路行为不变。
Co-Authored-By: Claude <noreply@anthropic.com>
…ge 恒 null codex review PR #638 指出:#1/#3 改在 codex adapter/generic 路径,但 B 模式目标是 codex-app,两处在 codex-app 上都不生效: 1. #1 model/reasoningEffort:codex-app buildArgs 之前没解构 model/reasoningEffort, app-server 从没收到覆盖。修:codex-app buildArgs 透传 --model/--reasoning-effort 给 runner;codex-app-runner 解析后注入 thread/start(model 走 thread-level、 model_reasoning_effort 走 config;xhigh→codex high)。(codex.ts 的注入保留,覆盖 纯 codex/RPC 路径。) 2. #3 usage:resolveSessionTranscriptPath 的 switch 无 codex-app case, getSessionTokenUsage 对 codex-app 恒 null(不是偶发 miss)。修:transcript-resolver 加 codex-app case(与 codex 同路径——codex-app 驱动 codex app-server,rollout 同格式); cost-calculator usageKindForCli 加 codex-app→codex 映射。补回归单测(codex-app 解析 codex rollout、usage 非 null)。 cost-calculator 43 测绿;build 绿。#2 的 turnId/answer 窗口 codex 仍在核,findings 出来再改。 Co-Authored-By: Claude <noreply@anthropic.com>
…swers:[]}}}
codex 用 Codex 0.145 生成类型核实:requestUserInput 的 answer 不是扁平
{[id]: text},而是 { answers: { [questionId]: { answers: string[] } } }(每问一个
string 数组)。改 mapAnswerToRequestUserInput 输出该嵌套形态,单文本回复收敛到首
问题的 answers 数组,多问题仍日志告警。
model/effort 保持 7637961 的 thread/start.model + config.model_reasoning_effort
(codex 已确认 ThreadStartParams 顶层 model? + 任意 config? 协议可接受,且契合
"仅新建会话生效"语义——thread/start 每新会话一次,fold-in 不触发,正确忽略覆盖)。
build 绿;async/cost/trigger-api 100 测绿。#2 生命周期(answer 丢失窗口、resolved
ACK)等 codex 完整 findings 再定。
Co-Authored-By: Claude <noreply@anthropic.com>
deepcoldy
left a comment
There was a problem hiding this comment.
二轮代码审查结论:当前仍有 blocking findings,暂不建议合并。c7407957 已修正 requestUserInput answer schema,thread/start 的 model/config 也与 Codex 0.145 生成类型兼容;但 awaiting_input 的作用域、交付 ACK、身份关联、elicitation schema、重启生命周期,以及 usage 的 per-trigger 语义仍未闭环。
此外当前 PR 已与最新 master 冲突(GitHub mergeable_state=dirty;本地 merge-tree 冲突 codex-app.ts、runner-input.ts、codex-app-runner.ts、types.ts、worker.ts)。master 已引入 CodexAppTurnController / runner protocol 与 appTurnId/replyTurnId 分域、steer/restart freshness;建议先 rebase,再把交互状态机落到新 controller/protocol 上,避免在旧 activeTurn runner 上返工。
本地验证:git diff --check 通过;7 个定向测试文件共 158 tests 通过;pnpm build 通过。现有测试未覆盖 structured server request → answer → serverRequest/resolved、失败 ACK、普通非 async 会话、multi-turn trigger 关联或 daemon/worker 重启恢复。由于 GitHub 登录身份与 PR 作者相同,平台不允许提交 REQUEST_CHANGES,因此以 COMMENT 记录 blocking review。
| @@ -313,11 +425,35 @@ function handleServerRequest(msg: JsonObject): boolean { | |||
| return true; | |||
| } | |||
| if (method === 'item/tool/requestUserInput') { | |||
There was a problem hiding this comment.
P1 — 这里会挂住所有 codex-app 会话,不只 async trigger。 同一个 runner 也服务普通飞书会话、同步 trigger 和其他非轮询调用方;当前没有任何 asyncReturnSessionId/capability gate,而 daemon 只把 marker 暴露到 async trigger-result,普通会话没有 UI/answer 消费者。模型一旦 requestUserInput,该 turn 会永久等待。请在每个 turn/input 上冻结“允许程序化 awaiting_input”的能力;未开启时保留旧自动响应,或同时实现普通会话的人机交互通道。
| } | ||
| // Optimistically clear the pending marker; final_output will also clear it, and | ||
| // a re-poll before the turn resumes just won't find it pending (409 above). | ||
| ds.awaitingInteraction = undefined; |
There was a problem hiding this comment.
P1 — 不能在 IPC enqueue 后乐观清状态。 child.send() 成功只说明消息进了 worker IPC;后续 writeInteractionAnswer 可以返回 submitted:false、throw、worker crash,runner 也可能忽略 stale id。当前这些都只记日志,但调用方已经收到 200、轮询变 running、重试又变 409,held app-server request 会永久悬挂。请增加 worker/runner → daemon 的端到端 ACK,并以 Codex 的 serverRequest/resolved {threadId,requestId} 作为权威 commit;清状态前保持 answer 可按 interactionId 幂等重试。
| pendingInteractions.set(interactionId, { interactionId, kind: input.kind, finish: input.finish }); | ||
| emitMarker('awaiting_input', { | ||
| interactionId, | ||
| turnId: activeTurn?.nativeTurnId ?? undefined, |
There was a problem hiding this comment.
P1 — 这里发的是 app-server native turn UUID,不是 botmux/riff 的 turn identity。 同文件 final marker 已明确 native id 会破坏 wait map/回复路由,交互同样应携带本次 queued input 冻结的 clientUserMessageId/replyTurnId,并单独保留 appTurnId 仅用于协议匹配。现在 daemon 的 awaiting 又是 session 级单槽、lookup 不按 requested triggerId 过滤,所以 multi-turn/fold-in session 还会把后一个交互返回给前一个 trigger 查询。请绑定 replyTurnId/triggerId,并在 lookup 时匹配。
| authChallenge, | ||
| // v1: any answered text = accept + single-field content; cancel is only | ||
| // used on timeout/no-answer (handled daemon-side, not here). | ||
| finish: (text) => client.respond(msg.id, { action: 'accept', content: { answer: text }, _meta: null }), |
There was a problem hiding this comment.
P1 — 这个映射对通用 elicitation 不成立。 Codex 0.145 的 params 是 tagged union:form/openai-form 携带 requestedSchema,url 模式携带 url + elicitationId。无条件回 {content:{answer:text}} 通常不满足表单 schema,URL mode 也不是该响应形态。请把 mode/schema 暴露给调用方并按 schema 映射/验证;无法表达的 form 应 decline/cancel,URL auth 单独处理,不能用一个自由文本宣称覆盖任意 elicitation。
| kind: 'clarification' | 'confirmation' | 'authentication'; | ||
| finish: (answerText: string) => void; | ||
| } | ||
| const pendingInteractions = new Map<string, PendingInteraction>(); |
There was a problem hiding this comment.
P1 — pending map 没有协议级回收/恢复。 app-server 会在请求被响应或因 turn lifecycle 清理时发 serverRequest/resolved,但 runner 不消费它,也不在 turn completion/interrupt/restart 清理或重放。未答请求会留下 stale closure;daemon 的 awaiting 仅内存,daemon/worker 重连后 runner 可能仍 hold,但新 daemon 已丢 marker并永久报告 running;多个并发请求还会被 daemon 单槽覆盖。请用 requestId↔interactionId 关联消费 resolved,并定义 restart 的 replay 或 fail-closed 策略。
| let usage: { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreateTokens: number } | undefined; | ||
| if (isCompleted && stored) { | ||
| try { | ||
| const u = getSessionTokenUsage({ |
There was a problem hiding this comment.
P1 — 这仍不是当前 trigger 的 usage,且可能命中旧缓存。 Codex rollout 的 total_token_usage 是 session 累计快照;同一 session 有多个/fold-in async turn 时,这里会把历史 turn 全算进本 task。并且未传 fresh:true,getSessionTokenUsage 可在 15 秒 throttle 窗口直接返回完成前的缓存。建议 runner 捕获 thread/tokenUsage/updated,在对应 appTurn 完成时取 tokenUsage.last 并随 async completed result 持久化;至少不能把 session total 标成 per-trigger TaskTokenUsage。
|
Superseded — 不合并本 PR,拆成 A/B/C 三个干净 PR 分落。 codex 用 Codex 0.145 权威协议类型 review 出 5 组 awaiting_input P1(非 async 死锁 / answer commit-unknown 谎报 / turnId 用 app-server UUID 串 turn / elicitation tagged union 无脑当文本 / 重启并发生命周期缺口)+ usage 非 per-trigger + master 重构了 codex-app runner(新 CodexAppTurnController/protocol)。一锅端的本 PR 无法在单一状态机下验收,故拆分:
本 PR 的 6 条 inline review + 两轮讨论保留作为 B/C 的设计依据,不 force-push 覆盖。 |
feat(trigger): per-turn model + reasoningEffort 覆盖(Codex 家族·新建会话·PR A / 拆自 #638)
feat(trigger): codex-app per-turn token usage → trigger-result(PR B / 拆自 #638)
拆自原 PR deepcoldy#638(superseded)的第一块,单一状态机、影响面最小。 - TriggerRequest.options 加 model?:string + reasoningEffort?:'low'|'medium'|'high'|'xhigh'(校验:model ≤200 字符、effort 枚举)。 - 仅新建会话生效:trigger-session 首 fork 前 stamp 到 session;sessionAgentConfig 的 agentFrozen 冻结(?? 保留 override);init 携带 reasoningEffort → worker → adapter。 - codex(纯/RPC 路径):buildArgs 注入 `-c model_reasoning_effort=<effort>`(xhigh→high)。 - codex-app(B 模式目标):buildArgs 透传 --model/--reasoning-effort 给 runner; codex-app-runner 注入 thread/start——model 走 ThreadStartParams 顶层 model、effort 走 config.model_reasoning_effort(codex 已按 0.145 生成类型确认该 schema,且 thread/start 每新会话一次、fold-in 不触发,契合 fresh-spawn-only 语义)。 影响面:只走 async trigger 专用路径;model/effort 仅 codex/codex-app 消费,其它 CLI 忽略 reasoningEffort(buildArgs 不解构即丢弃)。校验/透传单测;build 绿;trigger-api 27 测绿。 Co-Authored-By: Claude <noreply@anthropic.com>
背景
为 riff「B 模式:botmux 驱动 codex」对齐 riff 原生驱动 codex(codex-bridge/json)的能力。方向已定:B 绑 codex-app(cliId='codex-app',走 codex app-server 结构化协议),clarification 能拿 codex 原生结构化负载、与 A 同源,而非 PTY 扫屏降级。本 PR 是 botmux daemon 侧供给的三样能力,走 async trigger(asyncReturnSessionId)专用路径。
改了什么
#1 per-turn model + reasoningEffort 透传
TriggerRequest.options加model?+reasoningEffort?: low|medium|high|xhigh(带校验)。sessionAgentConfig冻结 model,init 携带 effort → worker → codexbuildArgs注入-c model_reasoning_effort;xhigh收敛到 codexhigh。#3 trigger-result completed 带 usage
TriggerResponse加usage:{inputTokens,outputTokens,cacheReadTokens,cacheCreateTokens}(对齐 riff TaskTokenUsage),仅completed态发;getSessionTokenUsage从 transcript best-effort 读,仅已有 completed 结果时才读。#2 codex-app 结构化澄清 awaiting_input 态 + POST /answer
requestUserInput/elicitation改为解析 question/details/authChallenge、emit awaiting_input marker 并 hold;答复回来映回 codex-app respond。命令/补丁审批仍自动 approve(trusted,与 A 一致)。契约与 riff 现有 BotmuxClient/BotmuxInteraction 逐字段对齐。
影响范围
测试
备注
🤖 Generated with Claude Code