tmux 团队运行时架构:窗口分割、文件邮箱与任务编排
tmux 团队运行时架构:窗口分割、文件邮箱与任务编排
学习目标
- 理解 OMX 如何创建和管理 tmux 团队会话
- 掌握 worker 的启动流程和就绪检测
- 了解文件邮箱通信模式和分派队列
- 理解团队相位生命周期和任务编排
前置知识
本章聚焦 Oh-My-Codex 的具体工程实践,无需前置阅读。
项目实践
tmux 会话创建
伪代码(src/team/tmux-session.ts — createTeamSession):function createTeamSession(teamName, workerCount): assert tmuxAvailable() assert hasCurrentTmuxClientContext()
// 获取当前 tmux 上下文 currentSession = displayMessage("session_name") currentWindow = displayMessage("window_id") currentPane = displayMessage("pane_id")
// 分割窗口格 paneIds = [] for i in 0..workerCount-1: if i == 0: // 第一个 worker:水平分割 paneId = run(`tmux split-window -h -t ${currentPane}`) else: // 后续 worker:垂直分割(堆叠在右侧) paneId = run(`tmux split-window -v -t ${paneIds[0]}`) paneIds.push(paneId)
// 设置布局 run(`tmux select-layout -t ${currentWindow} main-vertical`) run(`tmux resize-pane -t ${currentPane} -x 50%`) // leader 占 50%
// 创建 HUD 窗口格(底部全宽) hudPane = run(`tmux split-window -v -f -l 10 -t ${currentWindow}`) run(`tmux send-keys -t ${hudPane} "omx hud --watch" Enter`)
// 注册 resize hook resizeHook = registerClientResizedHook(currentSession, hudPane)
// 标记每个窗口格的实例 ID for paneId in paneIds: run(`tmux set-option -t ${paneId} @omx_pane_instance_id ${generateId()}`)
return { name: `${currentSession}:${currentWindow}`, workerPaneIds: paneIds, leaderPaneId: currentPane, hudPaneId: hudPane, resizeHook: resizeHook }布局示意:
┌──────────────┬─────────┐│ │ Worker 1 ││ ├─────────┤│ Leader │ Worker 2 ││ ├─────────┤│ │ Worker 3 │├──────────────┴─────────┤│ HUD │└────────────────────────┘Worker 启动流程
伪代码(src/team/runtime.ts — startTeam):function startTeam(teamName, workerSpec, options): // 1. 状态初始化 stateRoot = resolveTeamStateRoot(options.leaderCwd) createTeamDirs(`${stateRoot}/team/${teamName}/`)
writeConfig({ workers: workerSpec, tmux_session: teamName }) writeManifestV2({ schemaVersion: 2, policy: options.policy }) initEventLog() initMailboxes(workerSpec.count) initDispatchQueue()
// 2. tmux 分割 tmuxSession = createTeamSession(teamName, workerSpec.count)
// 3. Worktree 预配 if options.worktree: for worker in workers: worktreePath = planWorktreeTarget(`${teamName}/${worker.name}`) ensureWorktree(worktreePath)
// 4. Worker 身份 for i, worker in workers: write(`workers/worker-${i}/identity.json`, { name: worker.name, worktreePath: worker.worktreePath, cli: worker.cli, role: worker.role }) write(`workers/worker-${i}/inbox.md`, "")
// 5. Worker 启动命令 for i, worker in workers: startupScript = buildWorkerStartupCommand(worker, i) write(`runtime/worker-${i}-startup.sh`, startupScript) runInPane(tmuxSession.workerPaneIds[i], startupScript)
// 6. Worker 就绪检测 for i in 0..workerCount-1: waitForWorkerReady(tmuxSession.workerPaneIds[i], { timeout: 60s, backoff: "exponential" })
// 7. 信箱分派 for i, worker in workers: writeInbox(worker, buildFullInstructions(worker, teamName)) queueDispatchRequest(i) sendShortTrigger(tmuxSession.workerPaneIds[i]) // <200 charsWorker CLI 选择
伪代码:function resolveWorkerCli(workerIndex): // OMX_TEAM_WORKER_CLI_MAP(每 worker 独立) if env.OMX_TEAM_WORKER_CLI_MAP: map = parseCsvMap(env.OMX_TEAM_WORKER_CLI_MAP) if map[workerIndex]: return map[workerIndex]
// OMX_TEAM_WORKER_CLI(全局) if env.OMX_TEAM_WORKER_CLI: return env.OMX_TEAM_WORKER_CLI
// 默认 return "codex"支持三种 CLI:Codex、Claude CLI、Gemini CLI。“Prompt 模式”(非交互式)下不允许 Codex worker(需要 TTY)。
文件邮箱通信
通信模型:Leader → Worker: 1. 写完整指令到 workers/worker-N/inbox.md 2. 入队分派请求到 dispatch/requests.json 3. 通过 tmux send-keys 发送短触发信号(<200 chars)
Worker → Leader: Worker 通过 omx team api send-message 写入 mailbox/leader-fixed.json
Peer → Peer: Worker 写入 mailbox/worker-N.json分派队列状态机:
pending → notified → delivered → completed ↓ ↓ ↓ failed failed failedWorker 就绪检测
伪代码(waitForWorkerReady):function waitForWorkerReady(paneId, options): maxAttempts = 20 backoffMs = 500
for attempt in 0..maxAttempts: paneContent = capturePane(paneId)
if contains(paneContent, "trust prompt"): // Claude 信任提示 sendKeys(paneId, "y\n") continue
if contains(paneContent, "bypass prompt"): // Codex bypass 提示 sendKeys(paneId, "y\n") continue
if contains(paneContent, "viewport ready"): return { ready: true, attempts: attempt }
sleep(backoffMs) backoffMs = min(backoffMs * 2, 10000)
return { ready: false, reason: "timeout" }团队相位生命周期
相位控制器(src/team/orchestrator.ts)根据任务数量推断目标相位:
伪代码(phase-controller.ts):function inferTargetPhase(taskCounts): if taskCounts.pending == 0 && taskCounts.in_progress == 0 && taskCounts.blocked == 0: if taskCounts.failed == 0: return "complete" if taskCounts.fixAttempts < 3: return "team-fix" return "failed"
if taskCounts.in_progress > 0: return "team-exec"
if taskCounts.pending > 0: return "team-exec"
return "team-plan"任务认领锁
伪代码:function claimTask(taskId, workerId): lockDir = `claims/task-${taskId}.lock`
try: mkdir(lockDir) // 原子操作 catch EEXIST: return { claimed: false, reason: "Already claimed" }
try: // 检查任务是否真的可认领 task = readTask(taskId) if task.status != "pending": return { claimed: false, reason: "Task not pending" }
// 认领成功 task.status = "in_progress" task.assignee = workerId task.claimedAt = now() writeTask(task)
return { claimed: true } finally: rmdir(lockDir)原子状态写入
伪代码(writeAtomic):function writeAtomic(filePath, data): tmpFile = `${filePath}.tmp.${pid}.${timestamp}.${random}`
write(tmpFile, JSON.stringify(data)) fsync(tmpFile) // 确保写入磁盘 rename(tmpFile, filePath) // 原子替换问题与规避
| 问题 | 后果 | OMX 的规避策略 |
|---|---|---|
| Worker 窗口格不可用 | tmux 会话崩溃 | HUD resize hook 自动恢复 |
| Leader worktree 脏 | Worker worktree 预配不安全 | 断言 leader workspace 清洁 |
| 任务认领竞争 | 两个 worker 认领同一任务 | 目录级文件锁(mkdir 原子操作) |
| Worker 启动超时 | 团队会话不完整 | 回滚已预配的 worktree |
| 分派队列堆积 | worker 信箱溢出 | 分派队列 drain + 通知协调 |
设计取舍
为什么选择文件邮箱而非进程间通信(IPC)?
OMX 的 team 运行时使用文件(inbox.md、mailbox/*.json、dispatch/requests.json)作为 worker 间通信介质。
优势:
- 持久化:文件天然跨会话持久化,支持 resume
- 调试友好:直接
cat查看通信内容 - 跨 CLI 兼容:Codex、Claude CLI、Gemini CLI 都能读写文件
- 无额外依赖:不需要消息队列服务器
代价:
- 延迟:文件写入 + 轮询检测比 IPC 慢
- 并发控制:需要文件锁防止竞争
- 大小限制:大文件传输效率低
替代方案:
- Unix domain sockets / named pipes:低延迟,但跨平台兼容性差
- Redis / 消息队列:高性能,但增加部署复杂度
参考来源
- Oh-My-Codex v0.18.9 源码 —
src/team/tmux-session.ts(tmux 会话管理) - Oh-My-Codex v0.18.9 源码 —
src/team/runtime.ts(团队运行时核心) - Oh-My-Codex v0.18.9 源码 —
src/team/orchestrator.ts(相位控制器) - Oh-My-Codex v0.18.9 源码 —
src/team/state.ts(状态管理) - Oh-My-Codex v0.18.9 源码 —
src/cli/team.ts(team CLI 入口)