daemon 是常驻编排层(一个 bot 一个进程),worker 是一会话一进程的会话执行器,两者经 Node IPC 通信。协议类型集中在 src/types.ts:DaemonToWorker(types.ts:851)与 WorkerToDaemon(types.ts:940)。

Daemon 启动流程

入口 src/index-daemon.ts(core-only 变体为 src/index-core-only.ts):

  1. 启动卫生:安装 stdio EPIPE 守卫(pm2 下 stdout 是指向 God daemon 的管道,断管不能崩);加载 ~/.botmux/.env;scrub 会话级环境变量——daemon 永远不是会话,pm2 重启会把调用者(可能是某个 botmux 会话)的 BOTMUX_SESSION_ID、CLAUDE_CONFIG_DIR 等泄漏进常驻进程,导致 hook 自循环或 worker 读写错误 bot 的 home(index-daemon.ts:19-43)。
  2. 读全局 config.json 设 locale,然后动态 import daemon.js 调 startDaemon(botIndex)。

startDaemon()(daemon.ts:19954)关键步骤:

  • 清洗共享 tmux server 上的残留路由 env(scrubTmuxServerGlobalEnv)
  • 沙盒配置迁移(core-only 模式跳过,不许碰宿主 fleet 的 bots.json)
  • loadBotConfigs() / loadBotConfigAtIndex(idx) 认领本进程负责的 bot(一个 daemon 一个 bot),registerBot(cfg) 注册进 bot-registry
  • ensureCliEnv(cliId):为该 bot 的 CLI 预装 skills + askUserQuestion hook(幂等;adopt 路径会跳过,所以必须在启动时兜底)
  • sessionStore.init(larkAppId) 等按 bot 初始化存储;启动维护任务、Issue Board 出箱泵、CJK 字体安装
  • 为每个 bot 启动 startLarkEventDispatcher()(daemon.ts:20835):创建 Lark WSClient 长连接订阅事件(apiOnly bot 不订阅飞书事件,只开 HTTP 控制 API)

消息分发入口

飞书事件经 im/lark/event-dispatcher.ts(3880 行)准入判定:bot 身份探测、群权限、@mention 解析、话题/普通群模式,然后回调进 daemon.ts 的路由函数:

  • handleNewTopic(daemon.ts:16495)— 新话题开新会话
  • handleThreadReply(daemon.ts:17781)— 话题回复路由回既有会话
  • handleBotAdded(daemon.ts:17371)— bot 进群自动激活(CAS 注册 + ready 屏障 + 接管检测)

斜杠命令由 core/command-handler.ts 拦截;普通文本进入会话:无 worker 则 fork,有则经 IPC 转发。

Worker fork 与启动

core/worker-pool.ts 的 forkWorker(ds, promptInput, resumeOrTurnId)(worker-pool.ts:5769)是唯一 fork 入口,前置守卫包括:路由 transfer 门、设备隔离冻结、隔离区(quarantine)计划、双 fork 守卫(存活 worker 不可被杀替)。实际 fork:

fork(worker.js, [], { stdio: ['ignore','pipe','pipe','ipc'], cwd, env })

env 注入 BOTMUX=1、BOTMUX_SESSION_ID、SESSION_DATA_DIR、LARK_APP_ID(无传输会话刻意置空 secret,worker-pool.ts:6257)。fork 后 daemon 发 init 消息,携带会话全量配置(sessionId、workingDir、cliId、backendType、sandbox 策略、prompt、turnId、larkAppId/Secret 等,types.ts:852)。

worker-ipc-preload.ts:worker 正式 IPC 处理器注册前,preload 脚本暂存 daemon 消息;识别普通冷启动首轮(turnId 以 om_ 开头)立即回 turn_input_received 传输回执,handler ready 后按原序重放——保证慢启动期间不丢消息、回执语义不变。

Worker 内部生命周期

worker.ts 头部注释概括了完整生命周期:

  1. 被 daemon fork,收 init 消息
  2. 经 createCliAdapterSync(adapters/cli/registry.ts)创建 CLI 适配器 + 选择后端(PtyBackend/TmuxBackend/zellij/herdr/riff),backend.spawn() 拉起 CLI(worker.ts:13201);sandbox 开启时包一层 seatbelt/bwrap profile
  3. 启动 HTTP + WebSocket server 服务 xterm.js Web 终端(worker.ts:173 WebSocketServer)
  4. 回 ready{port, token};之后持续把 PTY 输出渲染成 screen_update 事件推给 daemon(→ 流式卡片,见 card-streaming)
  5. 收 message/raw_input 事件写入 CLI stdin(带输入队列、去重、durable turn 确认:turn_input_committed)
  6. close → 杀 CLI 退出;restart → 杀 CLI 并以 --resume 重拉(可携新 cwd/env);suspend → 释放 CLI 保留会话记录
  7. CLI 退出时发 claude_exit{code, signal, logTail}(worker.ts:13859),daemon 侧据此决定自动重启或 park_diagnostic 停一个诊断 shell(崩溃循环兜底)

生命周期治理

  • 空闲回收:core/idle-worker-sweeper.ts,默认每 bot 最多 30 个 live worker(DEFAULT_MAX_LIVE_WORKERS),超额 LRU suspend(CLI 释放,下条消息冷恢复 transcript)
  • 定时:core/scheduler.ts 用 croner 驱动 schedule-store 中的任务,到点在原话题续消息触发新会话;同时发 schedule.fired hook
  • 心跳/维护:daemon-heartbeat、maintenance、CLI 运行时更新监控、主机过载告警(startDaemon 内装配)

关键 services

服务职责
services/session-store.ts会话持久化(按 bot 分文件),Session 行的读写与所有权校验
services/message-queue.ts每话题一个 JSONL 持久队列(queues/.jsonl + offset),worker 忙时排队、恢复重放
services/hook-runner.tshooks 事件(topic.new/thread.reply/session.start/exit/idle/…)外发,本地 spawn 或经 daemon IPC
services/usage-ledger.ts每轮 token 增量追加到 ~/.botmux/usage/usage-YYYY-MM-DD.jsonl,基线锚定在 worker spawn 时刻
services/schedule-store.ts定时任务持久化(供 scheduler 消费)
services/claude-transcript.tsClaude 系 transcript JSONL 解析(输出回退、指纹、轮次切分),其他 CLI 有各自 transcript 服务

消息路由细节见 im-routing,适配器与后端见 adapters,会话模型见 session-model。