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):
- 启动卫生:安装 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)。 - 读全局
config.json设 locale,然后动态 importdaemon.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-registryensureCliEnv(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 头部注释概括了完整生命周期:
- 被 daemon fork,收
init消息 - 经
createCliAdapterSync(adapters/cli/registry.ts)创建 CLI 适配器 + 选择后端(PtyBackend/TmuxBackend/zellij/herdr/riff),backend.spawn()拉起 CLI(worker.ts:13201);sandbox 开启时包一层 seatbelt/bwrap profile - 启动 HTTP + WebSocket server 服务 xterm.js Web 终端(worker.ts:173
WebSocketServer) - 回
ready{port, token};之后持续把 PTY 输出渲染成screen_update事件推给 daemon(→ 流式卡片,见 card-streaming) - 收
message/raw_input事件写入 CLI stdin(带输入队列、去重、durable turn 确认:turn_input_committed) close→ 杀 CLI 退出;restart→ 杀 CLI 并以--resume重拉(可携新 cwd/env);suspend→ 释放 CLI 保留会话记录- 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.firedhook - 心跳/维护:daemon-heartbeat、maintenance、CLI 运行时更新监控、主机过载告警(startDaemon 内装配)
关键 services
| 服务 | 职责 |
|---|---|
services/session-store.ts | 会话持久化(按 bot 分文件),Session 行的读写与所有权校验 |
services/message-queue.ts | 每话题一个 JSONL 持久队列(queues/ |
services/hook-runner.ts | hooks 事件(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.ts | Claude 系 transcript JSONL 解析(输出回退、指纹、轮次切分),其他 CLI 有各自 transcript 服务 |
消息路由细节见 im-routing,适配器与后端见 adapters,会话模型见 session-model。