Skip to content

feat(trigger): HTTP API 支持 options.steer(codex-app 原生 turn/steer) - #1445

Merged
deepcoldy merged 3 commits into
masterfrom
feat/http-steer
Sep 18, 2026
Merged

deepcoldy merged 3 commits into
masterfrom
feat/http-steer

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

背景

core-only 公开面(POST /api/trigger + 两条 poll 路由)此前无法使用 codex app-server 的原生 turn/steer:忙时把消息注入当前活跃 turn(而不是串行排队等它结束)的授权(codexAppSteerable)只在飞书 plain-human 入口计算(computeCodexAppSteerable),HTTP 触发路径从不透传。headless 调用方(如 riff core-only)在任务运行中发追问只能排队等当前轮跑完再起下一轮。

契约

POST /api/triggeroptions 新增 steer?: boolean(默认 false,与 dryRun 互斥);响应在实际受理(非幂等复用)时回显 steer: true

  • 首轮:opening root 标记为 steerable(codex canSteer 要求 root 与 follow-up head 显式授权)。
  • 运行中追问(worker live):runner 用 turn/steer 注入活跃 turn;无法注入(无活跃 turn / closing / review·compact turn / 休眠冷启 / 非 codex-app CLI)时静默降级为普通排队 follow-up,受理不因 steer 报错。
  • 非 codex-app CLI:no-op,保持既有 type-ahead 队列语义。

结果归因(steer 组)

codex 对合并的 steer 组只产出一条合并 final:runner 给前 N−1 个成员发空的 steer_superseded final,最后一个成员(=最新 triggerId)拿真 final + usage。

  • Lark sink:旧行为不变(superseded 只推进 FIFO,不投递)。
  • HTTP sink(http_async / http_wait,本 PR 新增):superseded 成员先 park(内存 fanout 表 + async-trigger store 的 steerParkedBy 后继指针链,仅 http_async 写 durable);真 final 落盘后把同一份合并正文 fan-out 给组内每个 parked 成员(wait promise resolve / async result completed + recordCompleted不带 usage——用量只记最新 trigger),并清 idempotentAsyncTurns 收敛项,避免随后优雅退出把已合并轮误判失败。
  • daemon 在「superseded pop → 真 final」之间重启:poll 时沿 steerParkedBy 链走到首个终态后继并镜像(completed 镜像正文、failed 镜像终态证据);链仍 pending 则继续 running

安全面

不新增公开路由,core-only 仍是 loopback 的「驱动我自己的 turn」面;steer 只授权注入同一租户自己的活跃 turn。R4/R5 的 superseded 守卫从仅 lark 扩到 lark | http_async | http_wait,VC / doc_comment / suppressed / sink 缺失 / 唯一 head 仍一律 ACK=false 拒绝。

测试

  • trigger-api:steer 校验 3 例(布尔类型、与 dryRun 互斥、fresh/follow-up 受理)
  • trigger-session-turn-idempotency:live/dormant/无 flag plumbing 3 例
  • bridge-final-output-retry:R4/R5 守卫矩阵更新(http_wait 不再拒绝)+ http_async / http_wait 全组 fan-out 2 例
  • async-trigger-store:park / N 链 / 终态强证据不覆写 / 严格 schema 4 例

设计文档:docs/design/2026-07-30-api-only-core-only-bot-mode.md 增补一节。

🤖 Generated with Claude Code

deepcoldy and others added 2 commits September 17, 2026 08:06
POST /api/trigger 新增 options.steer 布尔位,把飞书 plain-human 入口已有的
codex-app turn/steer 能力开放给 headless HTTP 调用方(riff core-only):

- trigger-session:live worker 的三条下发路径(wait/async/plain)与三条
  dormant fork 路径透传 codexAppSteerable;fresh turn 标记 opening root
  (canSteer 要求 root 与 head 同时授权);响应在实际受理时回显 steer:true,
  幂等复用不回显。无法注入时 runner 静默降级为普通排队 follow-up。
- worker-pool:steer 组 superseded final 的 daemon 守卫从仅 lark sink 扩到
  http_async/http_wait;superseded HTTP 成员先 park(内存 fanout 表 +
  async-trigger store 的 steerParkedBy 后继链),组内真 final 落盘后把合并
  正文 fan-out 给每个 parked 成员(completed 不带 usage、清 convergence 项;
  wait promise 同步 resolve)。
- dashboard-ipc-server:daemon 在 superseded→real-final 之间重启时,
  trigger-result 沿 steerParkedBy 链找到首个终态后继并镜像(completed 镜像
  正文、failed 镜像终态证据),链仍 pending 则继续 running。
- async-trigger-store:新增 recordSteerParked + PersistedAsyncTriggerResult.
  steerParkedBy(仅 pending、严格 schema 校验、completed/failed 强证据不覆写)。

非 codex-app CLI 上该 flag 为 no-op(既有 type-ahead 队列语义不变);不新增
公开路由,core-only 信任面不变(同租户自己的 turn)。

测试:trigger-api 校验 3 例;trigger-session live/dormant/无 flag plumbing 3
例;bridge R4/R5 守卫更新(http_wait 不再拒绝)+ http_async/http_wait 全组
fan-out 2 例;async-trigger-store park/链/schema 4 例。

Co-Authored-By: Claude Code <noreply@anthropic.com>
review (#1445) minor-1:followSteerParkedChain 原来 8 跳封顶,daemon 在 >8
成员的 steer 组中段重启时,早期成员沿链走不到终态(session open 一直 running、
closed 落 no_output)。成员数本就无界,跳数限制只在重启兜底路径上误伤。

- 链遍历从 dashboard-ipc-server 下沉为 async-trigger-store 导出函数,去掉跳数
  上限,改用 visited 集合保证终止:FIFO 后继天然互异且更晚,revisit 只可能是
  盘上损坏的环,遇环 fail-closed(undefined → 继续 running,不空转)
- 缺 hop / 链尾仍 pending 同样返回 undefined(running 语义不变)
- 测试 +4(12 成员长链、损坏环、缺 hop/链尾 pending、failed 终态后继),
  原有 N 链用例改调真实导出函数
- 文档补充两个已接受边界:http_wait 纯内存无 durable(wait 既有契约);后继
  指针若指向交错的非 steer 普通 turn,parked 成员镜像其 final(归因合理)

Co-Authored-By: Claude Code <noreply@anthropic.com>
@deepcoldy

Copy link
Copy Markdown
Owner Author

自动评审结论:设计与实现整体成立,无阻断性问题,有 1 个建议合入前补齐的测试缺口,加 2 处文档/命名小修。以下为初步意见,最终以维护者审阅为准。

建议合入前处理

1. 重启保险的 poll 镜像路径缺少路由级测试

buildAsyncTriggerLookupResponsesrc/core/dashboard-ipc-server.ts)中的 steerParkedBy 镜像块承载着「daemon 在 superseded → real-final 之间重启」这一核心承诺,但独立变异验证(整块删除后跑相关测试)下现有测试全部通过——store 原语(followSteerParkedChain 的长链 / 损坏环 / 缺 hop / failed 后继)覆盖充分,但调用方的三个镜像分支没有被钉住:

  • completed 后继 → 镜像正文(不带 usage)
  • reason === 'turn_terminal' 后继 → 镜像 terminalErrorCode
  • 其余 failed 后继 → dispatch_unknown

以及 owner 回退(persisted.ownerLarkAppId ?? cachedLarkAppId)和「链仍 pending 时保持 running」(重启窗口尚未关闭)这两个语义。建议补一条 trigger-result 路由级测试覆盖三态镜像 + 链 pending = running。

2. 公开 API 文档缺 options.steer

docs-site 中英文 api-task-trigger.md 的 options 有效字段表(dryRun / idempotencyKey 均有条目)没有 steer;英文文中两处 "steer" 是 prompt steering 的无关散文。headless 调用方只看公开文档无法发现新契约,建议补上字段行与一小节说明:与 dryRun 互斥、fresh / follow-up 语义、runner 不可注入时 best-effort 静默降级排队、superseded 组成员轮询拿到合并正文且不带 usage。设计文档增补节已写得很完整,公开页对齐即可。

3. 拒绝码字符串语义已过时(零风险改名)

superseded_head_not_plain_lark_steerable_with_successorsrc/core/worker-pool.ts)在本 PR 将 http_async / http_wait 纳入合法 sink 后已名不副实。该字符串全仓仅一处使用、只进 worker 日志,改名无风险(如 superseded_head_not_authorized_sink_with_successor)。

可选(不阻塞合入)

  • steerFanoutBySession 在组内最后一轮走 turn_terminal 失败时不会被清理(每个异常会话遗留一个小对象,可在 ledger drain / 会话 close 处顺手清)。
  • http_wait parked 成员在整组失败时只能等满自己的 wait timeout(wait 模式既有 best-effort 契约,可留作 fast-follow)。
  • 两处建议在注释中显式写明的不变量 / 边界:
    • fanOutSteerGroupFinal 只按 sessionId + generation 匹配 park 表,且对任意非 superseded final 都会触发;其正确性依赖「同一 generation 内后续输入只会 steer 入组、不存在旁路独立派发」这一 FIFO 不变量,写明后可防未来误用;
    • steerParkedBy 指向的后继若为 lark turn(盘上无 async 记录),重启链必断,该成员最终由 session closed → failed(no_output) 收敛——失败方向安全,仅「重启窗口 + 真人插话交错」同时命中,可作为已接受边界落注释。

验证情况

  • 已 rebase 到评审时最新 master,无冲突,patch-id 与原推逐字一致;tsc 0 错误。
  • 相关测试本地全部通过:PR 四个测试文件 200 例,另加 steer / codex-app dispatch / dashboard IPC / async state 等回归共千余例。
  • 变异验证:store / validator / trigger-session plumbing / worker-pool fan-out 的新测试在旧实现下均如期变红;唯一删除后不变红的就是上面第 1 点的镜像块。
  • PR 头 CI 唯一失败用例 test/turn-reply-ask.test.ts(150ms 真实定时器时序断言)与本 PR 无文件 / 符号交集,本地多次复跑稳定通过,判断为与本 PR 无关的既有时序 flake。

双审 (#1445) 收敛后的 follow-up,无行为面变更:

- F1:新增 trigger-result 路由级测试 6 例,钉住 daemon 重启于
  superseded→真 final 窗口时 buildAsyncTriggerLookupResponse 沿
  steerParkedBy 链的三态镜像(completed 合并不带 usage / turn_terminal
  镜像 provider code / 其余 dispatch_unknown)、链仍 pending 与盘损环
  fail-soft 保持 running;此前该镜像胶水整块删除测试仍全绿。
- 镜像 strict/durable 写入包 try/catch:EIO/owner 不符时本次 poll 保持
  running 不返 500,下次 poll 重试(与既有 postBarrierFault 收敛同形),
  由 writefault 用例变异钉住。
- N3:onCodexAppLedgerDrained 单一收口 prune 失败形态残留的内存 park 表
  (worker-pool 导出 pruneSteerFanoutState,daemon 收口调用),钉一例:
  drain 后迟到的真 final 不得再 fanout 给已走自收敛路径的 parked 成员。
- N1:superseded 拒绝码改名 not_plain_lark_… → not_authorized_sink…
  (http_async/http_wait 已合法化,原名误导;全仓仅一处日志引用)。
- 注释/文档:fanOutSteerGroupFinal 写明组身份依赖的「同 generation 无旁路
  派发」FIFO 不变量;recordSteerParked 写明 lark 后继断链已接受边界;
  docs-site 中英 api-task-trigger 补 options.steer 字段行与 §5.5;设计
  文档增补节同步 fail-soft/prune/两条边界的最终口径。

验证:tsc 0、build 过;新增 7 例对「无镜像块」4 红、对「无 fail-soft」
1 红、prune no-op 1 红,恢复全绿;相关回归 1080+ 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
@deepcoldy
deepcoldy merged commit f50012a into master Sep 18, 2026
13 of 14 checks passed
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