GBrain Minions — 源码深度分析
> 一句话版本:一个 Postgres 原生的后台任务队列系统,灵感来自 BullMQ,但完全不需要 Redis。确定性工作(shell 脚本、数据同步)走 Minions 零 token 消耗,判断性工作(LLM 循环)也可以用 subagent handler 在同一队列里跑。4,448 行 TypeScript。
| 项目 | 信息 |
|---|---|
| 来源 | [github.com/garrytan/gbrain/src/core/minions/](https://github.com/garrytan/gbrain/tree/master/src/core/minions) |
| 语言 | TypeScript |
| 依赖 | PGLite(本地)/ Supabase(生产),Bun 运行时 |
| 灵感 | BullMQ(Redis 队列),Sidekiq(backoff 策略) |
架构总览
┌─────────────────────────────────────────────┐
│ MinionQueue │
│ (Postgres-native job queue) │
│ │
│ add() → claim() → completeJob() / failJob() │
│ cancelJob() → recursive CTE │
│ handleStalled() / handleTimeouts() │
└──────────────┬──────────────────────────────┘
│
┌─────────┼──────────┐
▼ ▼ ▼
Worker Worker Worker
(concurrency=1) (concurrency=4)
│
├── shell handler → /bin/sh -c "cmd"
├── subagent handler → Anthropic Messages loop
└── aggregator handler → fan-in child results
核心组件
1. MinionQueue(~1,500 行)— Postgres 原生任务队列
不需要 Redis,所有状态都在一张 minion_jobs 表里。
Job 生命周期(9 种状态):
waiting → active → completed
→ failed → delayed → waiting (retry)
→ dead (永久失败)
waiting → delayed (带延迟的任务)
active → paused → waiting (恢复)
waiting → waiting-children (父任务等子任务)
→ cancelled (递归取消所有后代)
active → dead (超时 / max_stall 超限)
关键设计:
| 设计点 | 实现方式 |
|---|---|
| **乐观锁** | `lock_token` + `lock_until`,worker 每半轮续约 |
| **FOR UPDATE SKIP LOCKED** | claim 时用 PG 行级锁,多 worker 安全 |
| **幂等提交** | `idempotency_key`,相同 key 返回已有 job |
| **事务原子性** | `completeJob()` 在同一个事务里:更新状态 + token 汇总 + 写 child_done + 解析父任务 |
| **递归取消** | `WITH RECURSIVE` CTE 一次性取消整棵任务树(最深 100 层) |
| **级联失败策略** | `on_child_fail`: `fail_parent` / `remove_dep` / `ignore` / `continue` |
token 汇总:子任务完成后自动向上汇总 token 消耗到父任务,最终可以看到整棵树的总成本。
2. MinionWorker(~250 行)— 并发 worker
const worker = new MinionWorker(engine, {
concurrency: 4, // 同时跑 4 个 job
lockDuration: 30000, // 30s 锁
stalledInterval: 30000, // 30s 检测一次 stalled
});
worker.register('sync', syncHandler);
worker.register('shell', shellHandler);
worker.start(); // 阻塞直到 SIGTERM
每个 job 独立的 AbortController:
signal— 超时 / 取消 / 锁丢失时触发shutdownSignal— worker 进程收到 SIGTERM 时触发(仅 shell handler 用)
Worker 循环:
1. 推进 delayed 任务
2. Claim 新 job(不超过 concurrency)
3. 检测 stalled(lock 过期但 status 仍 active → 重新排队)
4. 检测 timeout(timeout_at 过期 → dead-letter)
3. Shell Handler(~311 行)— 执行 shell 命令
双重安全门:
1. MinionQueue.add() 拒绝 name='shell',除非 allowProtectedSubmit=true(CLI 和本地 MCP 才有)
2. 环境变量 GBRAIN_ALLOW_SHELL_JOBS=1 才注册 handler
环境隔离:
// 只传递白名单变量,防止 $OPENAI_API_KEY 泄漏
const SHELL_ENV_ALLOWLIST = ['PATH', 'HOME', 'USER', 'LANG', 'TZ', 'NODE_ENV'];
优雅终止:
SIGTERM → 5 秒等待 → SIGKILL
stdout/stderr 用 UTF-8 安全的 TailBuffer 截断(64KB / 16KB)。
4. Subagent Handler(~710 行)— LLM 循环
不只是 shell,Minions 也能跑 LLM。完整的 Anthropic Messages API 循环:
- 工具调用 → 执行 → 结果回传 → 继续
- Crash-resumable:
subagent_messages+subagent_tool_executions持久化,worker 崩溃后从 pending 工具继续 - Rate leases:每次 LLM 调用前获取 lease,防止并发超限
- Token 汇总:每轮更新 token 计数
5. 辅助系统
Backoff(Sidekiq 风格):
- 指数退避:
2^(attempt-1) * delay - 固定退避:
delay - 抖动:
±jitter * delay
Stagger(确定性交错):
- FNV-1a 哈希 key → 0-59 分钟偏移
- 同一 cron 触发的 10 个 job 自动错开
Quiet Hours(静默时段):
- IANA 时区感知,支持跨午夜窗口(22:00-07:00)
defer策略:延迟 15 分钟重试skip策略:直接取消
Rate Leases(限流):
- Postgres 行级 lease,带 TTL
- 崩溃恢复免费(过期自动释放)
pg_advisory_xact_lock防止并发竞争- 3x 指数退避续约(250ms / 500ms / 1s)
父子任务 DAG
parent (waiting-children)
├── child_1 (completed → child_done → inbox)
├── child_2 (failed → child_done → on_child_fail policy)
└── child_3 (active → timeout → dead → child_done)
Aggregator 模式:
parent waits for ALL children → reads inbox → aggregates results
关键机制:
- 子任务完成/失败/超时/取消 → 自动向父任务 inbox 写
child_done消息 - 父任务读 inbox 收集结果
- 所有子任务 terminal → 父任务自动从
waiting-children→waiting on_child_fail策略决定一个子任务失败时父任务怎么办
Token 汇总:每个子任务的 token 消耗自动累加到父任务。
安全模型
| 层 | 保护 |
|---|---|
| Job name | `shell` 是受保护名,MCP 调用者不能提交 |
| 环境变量 | 白名单制,防止 API key 泄漏 |
| Shell 路径 | 硬编码 `/bin/sh`,防止 PATH 投毒 |
| 深度限制 | `maxSpawnDepth=5`,防止无限递归 |
| 子任务上限 | `max_children`,防止 fan-out 爆炸 |
| AbortController | 超时/取消/锁丢失/进程终止,四种信号 |
| Stall 检测 | `max_stalled`(默认 5),超过则 dead-letter |
与 BullMQ 对比
| 维度 | BullMQ (Redis) | Minions (Postgres) |
|---|---|---|
| 依赖 | Redis | PGLite / Supabase |
| 持久性 | Redis AOF/RDB | Postgres WAL |
| 分布式 | 天然支持 | 单 worker(当前) |
| 父子 DAG | 有限 | 完整(inbox + child_done) |
| 附件 | 无 | 内置(5MB 限制,SHA256) |
| LLM 循环 | 无 | 内置 subagent handler |
| 限流 | 外部 | 内置 rate leases |
| 静默时段 | 无 | 内置 quiet hours |
分析
优势:
- 🔥 零外部依赖——不需要 Redis,PGLite 2 秒启动
- 🔥 事务一致性——completeJob/failJob 把状态更新 + token 汇总 + child_done + 父任务解析全部包在一个事务里
- 🔥 Crash-safe——worker 进程被 SIGKILL,所有状态都在 Postgres,下次启动自动恢复
- 🔥 统一队列——shell + LLM + 聚合在同一系统里,parent-child DAG 跨类型
- 📊 完整可观测性——每个 job 有 progress、token 计数、transcript、附件
局限:
- ⚠️ 单 worker 进程——当前不支持多机分布式(没有 BullMQ 的多 worker 竞争)
- ⚠️ Bun 专属——TypeScript + Bun 运行时,不能直接用在 Node.js 项目
- ⚠️ Shell handler 不沙箱——明确说了"does NOT sandbox filesystem reads",信任边界是 cwd
- 🟡 subagent handler 只支持 Anthropic——硬编码
@anthropic-ai/sdk
与 Jay 的关联:
- 🔥 OpenClaw 的
sessions_spawn痛点——我们的 cron lint 就是 Garry 遇到的同样问题:19 个 cron 在网关超时下无法 spawn sub-agent - Minions 思路适用于 OpenClaw——确定性 cron 工作(deploy、lint、build)不需要 LLM,纯 shell 脚本就够了
- Rate leases 模式——如果 OpenClaw 要做 API 限流,lease 模式比 counter 更好(崩溃自动释放)
评分
| 维度 | 评分 (1-10) | 说明 |
|---|---|---|
| 设计质量 | 9 | 事务一致性、crash-safe、统一队列 |
| 代码质量 | 9 | 4,448 行,注释详尽,edge case 处理完善 |
| 创新性 | 8 | BullMQ 思路但 Postgres-native + 内置 LLM 循环 |
| 实用性 | 8 | Garry 生产验证(19 cron、45K 页 brain) |
| 通用性 | 6 | Bun 专属、单 worker、Anthropic-only |
| 与 Jay 的关联 | 9 | 直接解决 OpenClaw cron 痛点 |
| **总分** | **8.5** | Postgres 原生任务队列的标杆实现 |