本文是 Baton 双向工作流的唯一入口:用户输入如何经过 Controller 到达 Harness,Harness 的输出 如何成为可恢复事实并返回用户,以及 steer、Interaction、cancel、失败和恢复如何复用同一条 主路径。核心对象和不变量见 Kernel,Adapter 契约见 Harness。
控制(用户 → Harness)
User
→ chat-tui intent
→ Input admission / queue
→ Context Snapshot + Delivery
→ Delivery Attempt
→ Harness Adapter
→ Harness wire
感知(Harness → 用户)
Harness wire
→ Adapter normalize
→ Event append / broadcast
→ reduce / Projection
→ chat-tui render
→ User
Plugin 不另开执行通道。proposed-input 先展示给用户;只有用户确认或编辑并提交后,它才成为
普通 Input。Interaction 也不进入 prompt queue,而是按稳定 identity 就地解开等待方。
chat-tui 只把 composer 内容和用户意图交给 Baton。mention、Session 引用和 Plugin Context 在 Context 层解析;chat-tui 不理解 HarnessSession 或 Harness wire。
Controller 为 prompt Input 分配稳定 messageId 和 turnId,并用一等状态记录它的消费位置:
queued → admitted → finalized
│ │
│ └──────────────→ interrupted
└────────→ recalled
accepted_steer → finalized | interrupted
queued 输入仍可 recall。出队成为 admitted 后,用户消息已经是 BatonSession 的正典事实,
不能再伪装成“从未提交”;此后 Esc 表达 cancel/interrupt。队列只串行调度 driven Turn,Harness
自发产生的 observed Turn 不占 admission 槽。
Controller 出队时先 append:
user_message(source:user):保存用户原始输入;state_update(running, source:baton):为 driven Turn 开界。
这两个事实不等待 Harness 冷启动。否则用户消息会被进程启动时间绑住,Context prepend 也可能 被误写进正典历史。
当前 ContextSource 首先承载 BatonSession 的缺失历史。Controller 按目标 HarnessSession 的
已确认水位组装 (afterSeq, throughSeq],先持久化 ContextSnapshot,再选择 transport:
sync_context:Harness 提供独立同步能力;submit_side_channel:随本次sendTurn的 side channel 送达;prompt_prepend:都不支持时,在预算内 prepend 到 transport prompt。
Snapshot 只说明准备送什么。transport 接受后才 append ContextDeliveryReceipt 并推进该
HarnessSession 的 ContextEpoch;只有 Snapshot 没有 Receipt 时,下次必须重投。Context 注入
不修改 Codex、Claude Code 等原生 Session 文件,也不进入用户原始消息。
Harness 已打开、Context 已组装后,Controller 先 append
_baton_delivery_attempt_update(prepared),记录 Input、Target 和不可变
HarnessLaunchSnapshot;随后把 Attempt 推到 dispatching,再调用:
adapter.sendTurn(handle, promptInput)Adapter 根据自己的权威运行态返回:
new_turn:接受开启一轮新工作;steer:输入已经进入匹配的当前 Turn;rejected:没有接受责任,Controller 可以安全降级为 queued follow-up。
Receipt 只确认 Adapter 接受了投递责任,不代表 Harness 已完成。accepted 后的错误必须通过事件流
终结 Turn;throw 只能表示 Adapter 尚未接受。Controller 据此持久化 Attempt 的 accepted、
uncertain 或最终 outcome。
Adapter 消费原生 wire,把 message、thought、tool、diff、plan、task、usage、Interaction 和状态
翻译为 Baton Event 草稿。宿主在可信入口补齐 source:harness、HarnessTarget、HarnessSession
和 Turn 坐标,Store 再补 eventId、scope、时间与序号。
归一原则是“稳定语义 + raw 保真”:
- message/tool/plan 按稳定 ID upsert;
- 字段省略表示不变,
null/空集合表示清除,具体值表示替换,chunk 表示追加; - completed item 应携带全量内容,纠正此前丢失或乱序的 chunk;
- 原生粒度差异保留在
raw,Projection 与 Store 不出现 Harness 分支; - 未知终态悲观处理,未知通知进入有界诊断和原生 trace,不静默吞掉。
所有事件进入同一条路径:
append → broadcast → reduce → Projection snapshot → chat-tui
live 和重开 Session 使用相同 reducer。自愈也必须合成新的事实 Event 再走这条路径,不能直接 修改页面状态。chat-tui 只消费 transcript、activity、Interaction、status 等 view,不解析 Harness DTO。
正常完成、Harness error、子进程退出、transport close 和 cancel 最终都必须产生
state_update(idle, stopReason);错误路径先产生 _baton_error_update。Controller 按 turn ID
幂等 finalize:
- 持久化终态;
- 生成一次 Turn summary;
- finalize Delivery Attempt;
- 取消仍挂在该 Turn 上的 Interaction;
- 释放 driven Turn 并推进队列。
completed 但没有可见产出的空回合必须显式告警,不能表现成成功但无回复。
Harness 在没有用户 Input 时也可能产生后台结果。Adapter 以 Harness 来源的 running 开界,
以 idle 收界;Controller 只记账、持久化和投影,不把它放回输入队列。observed Turn 与 driven
Turn 共享 Event/Projection 主路径,因此 live 与 resume 都能看到相同结果。
follow-up 是 Controller 的排队策略,不是 Adapter 的另一套方法。用户在 Harness busy 时提交
第二条输入:
- 若目标支持且当前 turn identity 匹配,Controller 尝试
sendTurn; - Adapter 原生接受后,输入成为
accepted_steer,并以delivery:"steer"进入当前 Turn; - Adapter 拒绝、原生 race 或无法安全定向时,原输入只入队一次,当前 Turn 结束后作为新 Turn 执行。
Esc 只打断当前 driven Turn。已经接受的 steer 与该 Turn 共命运:cancel 后标记 interrupted,
不静默重发;仍在 queue 的 follow-up 保留并在当前 Turn 收口后继续。cancel 请求本身不等于完成,
最终以 Harness 的 idle/cancelled 为准;超过 cancel 宽限且 transport 状态足够明确时,Controller
可以合成终态兜底。
Interaction 表示“某个 requester 正在等待外部参与者给出结果”,当前 kind 包含 permission、 question 和 hook trust。完整闭环是:
Harness / Plugin request
→ kind-specific draft
→ Controller signs interactionId + requester
→ interaction.opened persisted
→ chat-tui presents to user
→ user / timeout / cancel resolves
→ interaction.resolved persisted
→ waiting Adapter or Resource reconcile continues
Harness Adapter 不自签 interaction ID,也不自行伪造 opened/resolved。resolution 就地解开等待方, 不进入 prompt queue。cancel、timeout、requester 带外解决和恢复清理都是显式 resolution。
Plugin Resource 请求用户决议时不在 Runner 中持有 Promise continuation。Baton 先持久化答案,再 重新 enqueue 原 Resource;下一次 reconcile 从持久 Interaction 读取结果。
自动 reviewer 没有向 Baton 打开 Interaction 时,审批回执是独立 ApprovalReview 审计事实,
不能伪造一组 opened/resolved;详见 审批生命周期。
如果进程断开时 Baton 无法证明 Harness 是否接受或完成,Attempt 保留 uncertain。恢复先观察
Harness 和 Event Ledger,再决定 finalize;不能为了让队列前进而无条件重投可能已有副作用的工作。
固定 wall-clock 超时会误杀合法长任务,因此 Baton 只把“长时间无事件”当作 stall signal:
- L1:记录 stall notice,使静默可见,但不自动 finalize;
- L2:Adapter 声明
reconcile时查询 Harness 权威运行态;只有明确 idle 才合成终态; - 无对账能力或结果 unknown 时交给用户继续等待或取消。
BatonSession 从 Event Ledger 重放 Projection、Attempt、Interaction 和 Context 水位。若原生 HarnessSession identity 仍可恢复,Adapter 使用它加速继续;否则新建原生 Session,并通过 Context delivery 补齐 BatonSession 历史。切换 Harness 也是同一机制,不需要复制粘贴上下文。
外部 HarnessSession 必须先由只读 Inspector 生成完整历史 Snapshot,再 adoption 为 BatonSession;此后 resume/fork 只走 BatonSession 主路径。详细边界见 Harness 和 resume 与 fork。
src/controller/input.ts、turn.ts、attempt.ts— Input、Turn 与 Delivery Attempt ownersrc/context/delivery.ts— Snapshot、Receipt 与 Epochsrc/controller/interaction.ts、src/interaction/types.ts— Interaction 生命周期src/event/、src/store/reduce.ts— Event 信封、append/reduce 与投影状态tests/input-lifecycle.test.ts、tests/delivery-attempt.test.ts— 输入与投递状态迁移tests/lifecycle.test.ts、tests/reconcile.test.ts— 终态与 stall 对账tests/cancel-cascade.test.ts、tests/harness-initiated-turn.test.ts— Interaction 级联和 observed Turn