Skip to content

feat(trigger): botmux 驱动 codex-app 三能力(per-turn model/effort + usage + 结构化澄清 awaiting_input) - #638

Closed
deepcoldy wants to merge 4 commits into
masterfrom
wt/v2-codex-app-daemon
Closed

feat(trigger): botmux 驱动 codex-app 三能力(per-turn model/effort + usage + 结构化澄清 awaiting_input)#638
deepcoldy wants to merge 4 commits into
masterfrom
wt/v2-codex-app-daemon

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

背景

为 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.optionsmodel? + reasoningEffort?: low|medium|high|xhigh(带校验)。
  • 新建会话生效:trigger-session 首 fork 前 stamp 到 session,sessionAgentConfig 冻结 model,init 携带 effort → worker → codex buildArgs 注入 -c model_reasoning_effortxhigh 收敛到 codex high

#3 trigger-result completed 带 usage

  • TriggerResponseusage:{inputTokens,outputTokens,cacheReadTokens,cacheCreateTokens}(对齐 riff TaskTokenUsage),仅 completed 态发;getSessionTokenUsage 从 transcript best-effort 读,仅已有 completed 结果时才读。

#2 codex-app 结构化澄清 awaiting_input 态 + POST /answer

  • runnerrequestUserInput/elicitation 改为解析 question/details/authChallenge、emit awaiting_input marker 并 hold;答复回来映回 codex-app respond。命令/补丁审批仍自动 approve(trusted,与 A 一致)。
  • worker:认 awaiting_input marker → send;新增 answer_interaction → writeInteractionAnswer 写回 runner。
  • daemon:resolver 加第 5 态 awaiting_input(优先 running、completed 仍胜、closed 忽略陈旧);trigger-result 带 interaction{interactionId,turnId,kind,question,details?,authChallenge?}。
  • 端点:POST /api/sessions/:id/answer body {interactionId,answer:{text}};dashboard 加代理(写 cookie 闸)。

契约与 riff 现有 BotmuxClient/BotmuxInteraction 逐字段对齐。

影响范围

  • 只走 async trigger 专用路径;不碰同步/webhook/普通会话/adopt/v3。
  • Codex 适配问题 #2 仅 codex-app 结构化路径;不碰纯 codex(PTY)/codex-rpc(traex)/claude;命令审批短路不变。
  • 跨 CLI 共用改动(types.ts 加可选 writeInteractionAnswer、runner-input 加 encode/writeRunnerAnswer)均为新增可选,不改既有签名。

测试

  • async 相关 74 测全绿;trigger-session/webhook 回归 42 测绿;pnpm build 绿。
  • ⏳ 现网端到端待配 codex-app bot 后走 canary 连测。

备注

  • botmux daemon 路线(runnerKind='botmux')的结构化澄清链,与 sandbox codex webshell 独立。
  • canary 待 review + 申晗「发」后基于 master 打 v3.5.3-canary.0。

🤖 Generated with Claude Code

deepcoldy and others added 4 commits July 28, 2026 11:57
对齐 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 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.

二轮代码审查结论:当前仍有 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。

Comment thread src/codex-app-runner.ts
@@ -313,11 +425,35 @@ function handleServerRequest(msg: JsonObject): boolean {
return true;
}
if (method === 'item/tool/requestUserInput') {

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 — 这里会挂住所有 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;

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 — 不能在 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 幂等重试。

Comment thread src/codex-app-runner.ts
pendingInteractions.set(interactionId, { interactionId, kind: input.kind, finish: input.finish });
emitMarker('awaiting_input', {
interactionId,
turnId: activeTurn?.nativeTurnId ?? undefined,

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 — 这里发的是 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 时匹配。

Comment thread src/codex-app-runner.ts
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 }),

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 — 这个映射对通用 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。

Comment thread src/codex-app-runner.ts
kind: 'clarification' | 'confirmation' | 'authentication';
finish: (answerText: string) => void;
}
const pendingInteractions = new Map<string, PendingInteraction>();

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 — 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({

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 — 这仍不是当前 trigger 的 usage,且可能命中旧缓存。 Codex rollout 的 total_token_usage 是 session 累计快照;同一 session 有多个/fold-in async turn 时,这里会把历史 turn 全算进本 task。并且未传 fresh:truegetSessionTokenUsage 可在 15 秒 throttle 窗口直接返回完成前的缓存。建议 runner 捕获 thread/tokenUsage/updated,在对应 appTurn 完成时取 tokenUsage.last 并随 async completed result 持久化;至少不能把 session total 标成 per-trigger TaskTokenUsage。

@deepcoldy

Copy link
Copy Markdown
Owner Author

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 覆盖。

@deepcoldy deepcoldy closed this Jul 28, 2026
deepcoldy added a commit that referenced this pull request Jul 29, 2026
feat(trigger): per-turn model + reasoningEffort 覆盖(Codex 家族·新建会话·PR A / 拆自 #638
deepcoldy added a commit that referenced this pull request Jul 29, 2026
feat(trigger): codex-app per-turn token usage → trigger-result(PR B / 拆自 #638
deepcoldy added a commit to LucasIcarus/botmux that referenced this pull request Jul 29, 2026
拆自原 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>
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