跳转到内容

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 chars

Worker 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 failed

Worker 就绪检测

伪代码(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 入口)