不是所有 Agent 都是”本地 TUI 进程”。botmux 有两类非 TUI 接入:

  1. runner 型适配器(mira / mir / codex-app):适配器的 resolvedBin 是 process.execPath,worker spawn 的是一个 Node runner 脚本;输入走 stdin 控制行,输出走 stdout 里的 OSC 控制帧。对 worker 来说它仍然”长得像个 CLI 会话”,复用同一套空闲检测与卡片流(见 card-streaming)。
  2. Codex 混合 RPC 引擎(codex-rpc-engine.ts):codex/traex 的 TUI 会话里,终端只当”显示器”,用户输入改走 app-server JSON-RPC,绕开 tmux 粘贴丢字节的终端层。

适配器注册面见 adapters。

Runner 协议:控制行进,OSC 帧出

输入(adapters/cli/runner-input.ts):worker 把每条消息编码成一行 ::botmux-<id>:<base64(JSON)>\n 写进 runner stdin。tmux 路径上必须分块(1KB/块)+ 节流(20ms),否则一次 send-keys 灌爆 pane pty 的 ~4KB N_TTY 缓冲,超时后无法证明字节是否落地。协议自带缓冲卫生:写前先发一个 Enter 冲刷上次失败写入遗留的半行(runner 只按换行清缓冲,半行会和下一条消息粘成坏帧);任何不确定写入返回 submissionDisposition: dirty_unknown,worker 据此围栏本代提交而不是盲目重试。

输出(adapters/cli/runner-control-channel.ts):runner 的显示文本与控制帧共享一条 PTY 字节流。控制帧格式 \x1b]777;botmux:<kind>:<base64>\x07(OSC 777),kind 有 thread(原生会话 id 绑定)、final(回合最终输出,带 startedAtMs/completedAtMs,可带 usage)、error 等。所有显示文本的 ESC 字节先替换成可见符 ␛,使控制帧边界结构性安全;RunnerControlDecoder 处理跨 PTY chunk 的半帧,超大/无终止候选直接打废而非无限缓冲。worker 侧 APP_RUNNER_OSC_CLI_IDS = {mira, mir}(worker.ts)启用该解码;codex-app 另走签名控制通道(见下文)。

三个 runner 骨架一致:读 stdin → 逐字符攒行(处理 Ctrl-C/退格)→ 控制行解码入队 → drainQueue 串行跑 turn → 每回合结束发 final 标记并重画 › 提示符(runner 的 readyPattern 就是 /›/)。

mira-runner.ts:Mira Web API(云端编排)

mira 适配器接 Mira(mira.bytedance.com)的云端搜索/编排 Agent,无本地可执行文件(registry 里 executable 为 undefined)。

  • 鉴权:优先 MIRA_COOKIE_HEADER / MIRA_SESSION 环境变量;否则用 sqlite3 直接读 Mira.app 的 Cookie 数据库(~/Library/Application Support/mira/Cookies),取 mira_session 等 cookie。
  • 会话:POST /chat/create(带 model/dataSources/topic)拿 Mira sessionId,发 thread 标记让 botmux 记住原生 id 供 resume(--mira-session-id)。
  • 回合:POST /chat/completion 拿 SSE 流,解析 title(首条打印 [mira] <title>)与 content 事件累积 final text,finish 事件收尾。模型解析三级回退:显式 --model/MIRA_MODEL → 用户设置 default_model → 模型元数据默认项。
  • 输出净化(mira-output.ts):剥掉 <cis-ctrl> 控制块(含 HTML 转义形态)、折叠空行;SSE 里没拿到正文时回退 chat/messages/round/last 历史接口(3 次退避重试)再提取最后一条 assistant 消息。

mir-runner.ts:本地 mircli Print Mode

mir 与 mira 是有意分开的两条路:mira 是云端编排(搜索/问答),mir 是本地 mircli 在本机干活(文件/bash 工具跑在用户机器上,依赖用户本地 miramcp bridge)。

选 Print Mode(mircli -p)而非驱动 TUI:没有启动确认提示、没有粘贴折叠、不需要会话状态 flush。每个 botmux turn spawn 一次:

mircli -p <content> --output-format text --session-id <sid> -y \
       --append-system-prompt <local-runtime-context>

多轮连续性靠 mircli 自己的 --session-id 存储。关键配套:

  • mir-local-runtime.ts:ensureMiramcpBridgeStarted 自动拉起本地 MCP bridge(探测 9801 端口、pid 文件、启动超时分级状态);ensureMiramcpSandboxAllows 修补 bridge 沙盒配置把 workspace 路径加进允许清单;getMiraRuntimePaths 给出真实 cwd/home 与逻辑别名。
  • 运行时系统提示:告诉模型真实 cwd、本地工具(mira_local_bash 等)在独立 bridge 进程里执行、相对路径不可靠(每条 bash 命令必须 cd <cwd> && 前缀)——都是实测踩坑后的约束。
  • prompt 解包(mir-prompt.ts):normalizeMircliPrompt 先拆掉 botmux 的路由信封,提取 <user_message> 并摘要 mention/角色信息再交给 mircli。
  • stdout 捕获 → stripAnsi → final 标记。局限:投递走 stdout 而非 botmux send,所以 mir 是被动参与者(能被 @ 但不能主动 @ 别人)。

codex-app-runner.ts:Codex App app-server(stdio JSON-RPC)

codex-app 把 Codex 桌面版的 app-server 协议接进飞书。runner spawn codex app-server --listen stdio://(AppServerClient,换行分隔 JSON-RPC),然后:

  • 启动序列:initialize(capabilities.experimentalApi: true)→ initialized → ensureThread:有 --thread-id 走 thread/resume,否则 thread/start;90s 启动总闸,超时 exit 2。
  • 回合模型:turn/start 参数由 codex-app-turn.ts 构造——approvalPolicy: never + sandboxPolicy: dangerFullAccess(botmux 本就 bypass 审批);支持 turn/steer:带 codexAppSteerable 显式授权的后续消息可在回合结束前 steer 进活跃 turn,形成”一个原生回合承载有序 accepted 组”,末位成员拥有真正的 final,前序成员结算为 superseded。身份关联用不可变的 clientUserMessageId(botmux/Lark turn id),与 codex 原生 turn id 严格分离——原生完成通知只有在携带精确 client id 时才可被认领。版本门控:additionalContext ≥ 0.135.0、clientUserMessageId ≥ 0.136.0,老版本回落 legacy prompt。
  • 服务端请求自动应答:item/permissions/requestApproval、item/tool/requestUserInput、mcp elicitation 等一律自动批准/取消,codex 永不等人。
  • token 统计:用量不走 turn/completed 而骑 thread/tokenUsage/updated 通知,按原生 turn id 进 TurnTokenUsageAccumulator(services/codex-app-token-usage.ts)累加,final 标记时 drain 附上 usage;数据异常则 poison 并显式报”用量丢弃”,不塞零值。
  • 签名控制通道:runner 的启动参数/私钥经 CODEX_APP_CONTROL_BOOTSTRAP_ENV 一次性文件引导后即从 env 抹掉(app-server 及其工具会继承 env);运行期状态经 unix socket 签名上报 + OSC 标记双通道,防止模型输出的文本伪造控制帧。

codex-rpc-engine.ts + codex-rpc-lifecycle.ts:混合 RPC 输入

针对普通 codex/traex TUI 会话的可选增强(codexRpcInput):tmux 面板里照跑 codex --remote ws://... resume <threadId> 当纯渲染端,另起一个 codex app-server --listen ws://127.0.0.1:<port> 引擎进程(每会话一个,detached、进程组清理,孤儿标记目录 ~/.botmux/data/codex-rpc-app-servers),输入经 turn/start(带 ack 的 RPC)投递——绕开 codex 启动期/设置抖动时丢 bracketed paste 的终端层。

协作语义(实测验证):app-server 把线程事件广播给所有打开该线程的连接,所以引擎先 thread/start 并打出第一个 turn(空线程没有 rollout,TUI 无法 resume,必须先落盘),TUI 再 resume 进来实时渲染。daemon 重启时引擎 thread/resume + 面板重新 spawn 指向新端口——旧面板连着已死的上一代 app-server,reattach 会冻死 Web 终端。

生命周期全部 fail-closed(codex-rpc-lifecycle.ts):

  • codexRpcEligible 门:仅 codex/traex + tmux 后端 + 非 adopt/沙盒/读隔离/disableCliBypass/wrapperCli/startupCommands;任何不满足退回普通粘贴路径,绝不静默变更能力或安全级别(RPC 硬编码 approvalPolicy=never,若绕过 disableCliBypass 门等于偷偷提权)。
  • orchestrateCodexRpcInit 四态状态机(accepted/ambiguous/resumed/not-engaged):fresh 首 turn 预发且绝不重排队(exactly-once);ambiguous 时明确告知用户”已发出未确认,不自动重发”。
  • paneRunsRemoteTui:走 pane 进程树读真实 argv 判断存活面板是不是 RPC 的 --remote TUI(不信任 tmux pane_current_command,读不到就 fail-closed 当原生面板)。
  • RpcEngagementFence 单调围栏防并发 engage 互相覆盖;每请求 60s 超时防 wedged app-server 挂死 flush 队列。

共同设计

三种实现共享同一套信念:投递确认要可证明(ack/rollout/transcript 增长,而非 sleep 后假设成功)、回合归属用不可变 client id(原生 id 只做关联)、不确定就 fail-closed 并告知用户。这是 botmux 在终端字节流这种不可靠介质上做出可靠会话管的底层逻辑,会话锚定与接管的更多背景见 im-routing。