feat: Minions adoption UX — smoke test + migration + pain-triggered routing

Teach OpenClaw when to reach for Minions vs native subagents. Ship three
pieces so upgrading from v0.10.x actually lands for real users:

- `gbrain jobs smoke` — one-command health check that submits a `noop` job,
  runs a worker, verifies completion, and prints engine-aware guidance
  (PGLite installs get the "daemon needs Postgres, use --follow" note).
  Fails loud if schema's below v7 so the user knows to `gbrain init`.

- `skills/migrations/v0.11.0.md` — post-upgrade migration file the
  auto-update agent reads. Six steps: apply schema, run smoke, ask user
  via AskUserQuestion which mode they want (always / pain_triggered / off),
  write to `~/.gbrain/preferences.json`, sanity-check handlers, mark done.
  Completeness scores on each option so the recommendation is explicit.

- `skills/conventions/subagent-routing.md` rewritten — was a "MUST use
  Minions for ALL background work" mandate, now reads preferences.json
  on every routing decision and branches on three modes. Mode B
  (pain_triggered) is the default: keep subagents until gateway drops
  state, parallel > 3, runtime > 5min, or user expresses frustration.
  Then pitch the switch in-session with a specific script.

Rename pass: "Minions v7" → "Minions" in README (JOBS block), TODOS.md
(P1 section header + depends-on), CHANGELOG.md v0.11.0 entry. v7 stays
as the internal schema version in code/migration contexts. The product
name is just Minions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-18 07:31:04 +08:00
co-authored by Claude Opus 4.7
parent 78eea2e3eb
commit 4e8a7671b0
6 changed files with 296 additions and 18 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ All notable changes to GBrain will be documented in this file.
## [0.11.0] - 2026-04-18
### Added — Minions v7 (agent orchestration primitives)
### Added — Minions (agent orchestration primitives)
Minions was a job queue. Now it's an agent runtime. Everything your orchestrator needs to fan out work across sub-agents without turning them into orphans or rate-limit disasters.
+2 -1
View File
@@ -328,12 +328,13 @@ EMBEDDINGS
LINKS + GRAPH
gbrain link|unlink|backlinks|graph Cross-reference management
JOBS (Minions v7)
JOBS (Minions)
gbrain jobs submit <name> [--params JSON] [--follow] Submit a background job
gbrain jobs list [--status S] [--queue Q] List jobs with filters
gbrain jobs get|cancel|retry|delete <id> Manage job lifecycle
gbrain jobs prune [--older-than 30d] Clean completed/dead jobs
gbrain jobs stats Job health dashboard
gbrain jobs smoke One-command health check
gbrain jobs work [--queue Q] [--concurrency N] Start worker daemon
ADMIN
+2 -2
View File
@@ -63,7 +63,7 @@
### ~~Constrained health_check DSL for third-party recipes~~
**Completed:** v0.9.3 (2026-04-12). Typed DSL with 4 check types (`http`, `env_exists`, `command`, `any_of`). All 7 first-party recipes migrated. String health checks accepted with deprecation warning + metachar validation for non-embedded recipes.
## P1 (new from v0.11.0 — Minions v7)
## P1 (new from v0.11.0 — Minions)
### Per-queue rate limiting for Minions
**What:** Token-bucket rate limiting per queue via a new `minion_rate_limits` table (queue, capacity, refill_rate, tokens, updated_at), with acquire/release in `claim()`.
@@ -76,7 +76,7 @@
**Effort:** ~2 hours. Deferred from v0.11.0 to keep the parity PR at a reviewable size.
**Depends on:** Minions v7 (shipped).
**Depends on:** Minions (shipped in v0.11.0).
### Minions repeat/cron scheduler
**What:** BullMQ-style repeatable jobs. `queue.add(name, data, { repeat: { cron: '0 * * * *' } })`.
+85 -14
View File
@@ -1,26 +1,97 @@
# Subagent Routing Convention
## Rule: Minions for all background work
How your agent decides between native subagents and GBrain Minions for
background work.
All background agent work MUST go through Minions (`submit_job`), never
`sessions_spawn` with `runtime: "subagent"`. The native subagent runtime
has no persistence, no observability, and no recovery.
## Rule: read the user's mode first
Every time you're about to spawn a subagent or submit a Minion, check the
user's preference:
```bash
cat ~/.gbrain/preferences.json 2>/dev/null
```
The file has `minion_mode` set to one of three values. If the file doesn't
exist, default to `pain_triggered`.
Then branch on mode:
### Mode A: `always`
All background agent work goes through Minions. `submit_job` is the default.
Never fall back to `sessions_spawn` with `runtime: "subagent"`.
Reference: `skills/minion-orchestrator/SKILL.md`
## Decision matrix
### Mode B: `pain_triggered` (default)
Default to native subagents. They're zero-setup, low-latency, and fine for
most tasks. Switch to Minions when one or more of these pain signals fires:
- **Gateway restart mid-task.** A subagent was running, the gateway crashed
or restarted, and you can't resume it.
- **State drop.** The user asks "what happened to the agent" or "why did
you forget what you were doing" and the underlying cause is the subagent
runtime losing context.
- **Parallel > 3.** The task needs more than 3 concurrent agents. Native
subagents start to drop at this width.
- **Long runtime.** The task is expected to take > 5 minutes. The user will
want to check progress, and polling a subagent doesn't give structured
progress the way `get_job_progress` does.
- **User explicit frustration.** The user says any of: "this keeps breaking",
"the agent keeps losing track", "why is this so flaky", "can we do this
in the background properly."
When ≥1 signal fires, pause and offer the switch:
> "Subagents keep dropping state on you. GBrain Minions is a durable queue
> for the same work — it survives gateway restarts, every job has progress
> tracking and a transcript, and you can pause / resume / steer running
> agents. Want me to flip this task to Minions? (~10s, no extra setup.)"
If the user says yes, submit the task as a Minion job with the same prompt.
Optionally propose flipping the default: `gbrain config set minion_mode always`.
### Mode C: `off`
Never suggest Minions. Use native subagents for everything. The user has
opted out. Respect it.
## Inline vs background (applies in all modes)
Even when Minions is the default (mode A), some work should run inline:
| Condition | Action |
|---|---|
| Single tool call, < 30s | Do it inline |
| Multi-step, any duration | Submit as Minion job |
| Parallel work (2+ streams) | Submit N Minion jobs with shared parent |
| Needs to survive restart | Submit as Minion job |
| User wants progress updates | Submit as Minion job with progress tracking |
| Research / bulk operation | Submit as Minion job, always |
| Single tool call, < 30s | Inline, always |
| Read-only query | Inline |
| User is waiting in real-time for the answer | Inline |
| Multi-step, user can walk away | Minion |
| Parallel 2+ streams | Minion (parent + children) |
| Needs to survive restart | Minion |
| User wants progress updates | Minion |
| Research / bulk operation | Minion |
**Rule of thumb:** if the user might ask "is it done yet?", use a Minion.
## Concurrency budget
- Check `get_job_stats` queue_health.active before submitting batch jobs
- If active > 5, submit remaining jobs with `delay` to stagger execution
- The resource governor auto-throttles, but don't dump 20 jobs at once
Before submitting batch jobs:
- Check `get_job_stats` queue_health.active
- If active > 5, stagger new jobs with `delay` so you don't swarm
- The resource governor auto-throttles but don't dump 20 jobs at once
## Flipping modes
The user can change their mind at any time:
```bash
gbrain config set minion_mode always # switch to always-on
gbrain config set minion_mode pain_triggered # back to default
gbrain config set minion_mode off # disable suggestions
```
Or edit `~/.gbrain/preferences.json` directly. The convention reads the file
on every decision, so changes take effect next tool call.
+162
View File
@@ -0,0 +1,162 @@
---
version: 0.11.0
feature_pitch:
headline: "GBrain Minions — durable background agents"
description: "Turn any long-running agent task into a durable job that survives gateway restarts, streams progress, and can be paused, resumed, or steered mid-flight. Postgres-native, zero infra beyond your existing brain. Replaces flaky subagent spawns for multi-step work, parallel fan-out, and anything the user might ask about later."
recipe: null
tiers: null
---
# v0.11.0 Migration: Minions
Minions is a job queue for agent work. Submit a task, get an id back. Check
progress whenever. The job keeps running even if the gateway restarts. Parent
jobs fan out to children, children post results back to the parent's inbox,
fan-in just works.
This migration adds the Minions schema (additive, safe to re-run), runs a
smoke test, and asks the user how they want their agent to use it.
## What's New
Minions primitives landed in this release:
- **Durability.** Jobs live in Postgres. Gateway crash = agent picks up where
it left off on restart. No lost state.
- **Observability.** Every job has structured progress, token accounting, and
a transcript. `gbrain jobs stats` is the health dashboard.
- **Steering.** Running jobs can receive inbox messages. "Focus on revenue,
skip headcount" lands as context on the next iteration.
- **Parallel fan-out.** Parent job spawns N children with depth + child-cap
guardrails (depth ≤ 5, max_children per parent). Children post `child_done`
into parent's inbox on completion. Parent polls and aggregates.
- **Per-job timeout.** `timeout_ms` + cooperative AbortSignal + DB-enforced
dead-letter. Agents that hang don't hold resources forever.
- **Cascade cancel.** Kill a parent → all descendants cancel via recursive CTE.
- **Idempotency.** Same key = same job. Safe to submit the same task twice.
Postgres only for the worker daemon (`gbrain jobs work`). PGLite supports
inline execution via `gbrain jobs submit --follow` but not background.
## Agent Instructions
### Step 1: Apply the schema
Minions needs schema version 7. If the user's brain is below that, bump it.
```bash
gbrain init
```
On existing Postgres brains this is additive (new columns via `ALTER TABLE
... ADD COLUMN IF NOT EXISTS`). On PGLite brains, the DDL bundle is re-applied
and the new columns appear.
If `gbrain init` errors, read the output and fix before continuing. Common
causes: stale lock file, wrong DATABASE_URL, PGLite directory permissions.
### Step 2: Smoke test
```bash
gbrain jobs smoke
```
Expected output on success:
```
SMOKE PASS — Minions healthy in 0.5s (engine: postgres)
```
On PGLite:
```
SMOKE PASS — Minions healthy in 0.3s (engine: pglite)
Note: the `gbrain jobs work` daemon requires Postgres. PGLite
supports inline execution only (`submit --follow`).
```
If SMOKE FAIL: report the exact error to the user, do not proceed. The most
common failure is "table not found, schema version 1 need 7" which means
Step 1 didn't actually run. Re-run `gbrain init` and try again.
### Step 3: Ask the user how they want to use Minions
Use AskUserQuestion. Re-ground first (project + branch + what Minions is),
then ask with these three options:
> GBrain Minions is a durable, observable job queue for background agent
> work. It survives restarts, tracks progress, and lets agents fan out
> parallel work with parent-child DAGs. How should your agent use it?
Options:
- **A) Switch to Minions for everything (recommended).** All background
work goes through Minions. Most durable, most observable. Adds some
overhead (Postgres queries per job) but pays off the first time the
gateway restarts mid-task. `Completeness: 10/10`
- **B) Use only when pain signals fire.** Default to native subagents
(lower overhead, zero extra setup). Auto-switch to Minions when the
gateway drops subagent state, the user asks "what happened to the agent,"
or > 3 parallel streams are needed. Agent watches for these signals and
proposes the switch in-session. `Completeness: 8/10`
- **C) Don't use Minions.** Keep subagents only. Turn this on later with
`gbrain config set minion_mode always` or `pain_triggered`.
`Completeness: 5/10`
RECOMMENDATION: Choose A for production brains and research-heavy workflows
(most durability, best transcripts). Choose B if the user is exploring
or on PGLite (lower friction, opt-in on demand). Choose C only if the
user explicitly says they don't want it.
### Step 4: Persist the choice
Write the user's answer to `~/.gbrain/preferences.json`:
```bash
mkdir -p ~/.gbrain
MODE="always" # or "pain_triggered" or "off" — substitute the user's pick
cat > ~/.gbrain/preferences.json <<EOF
{
"minion_mode": "$MODE",
"set_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"set_in_version": "0.11.0"
}
EOF
```
The subagent-routing convention (`skills/conventions/subagent-routing.md`)
reads this file at runtime. If the file is missing, the convention defaults
to `pain_triggered`.
### Step 5: Sanity-check the registered handlers
```bash
gbrain jobs work --concurrency 1 &
WORKER_PID=$!
sleep 2
gbrain jobs submit sync --params '{"dry":true}' --follow
kill $WORKER_PID 2>/dev/null
```
This proves the built-in handlers (`sync`, `embed`, `lint`, `import`) are
wired up. Skip on PGLite (use `--follow` instead of daemon).
### Step 6: Done
```bash
mkdir -p ~/.gbrain/migrations
echo '{"version":"0.11.0","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","status":"complete","minion_mode":"'$MODE'"}' >> ~/.gbrain/migrations/completed.jsonl
```
## For the user
If mode = `always`: "Your agent now runs every multi-step task through
Minions. `gbrain jobs stats` shows what's running. `gbrain jobs list` shows
everything. Nothing to configure."
If mode = `pain_triggered`: "Your agent keeps using subagents by default.
If the gateway drops state or you ask 'what happened to the agent,' it'll
offer to switch that task to Minions. You can flip modes any time with
`gbrain config set minion_mode always`."
If mode = `off`: "Minions is installed but dormant. Turn it on later with
`gbrain config set minion_mode always` or `pain_triggered`."
+44
View File
@@ -66,6 +66,7 @@ USAGE
gbrain jobs prune [--older-than 30d]
gbrain jobs delete <id>
gbrain jobs stats
gbrain jobs smoke
gbrain jobs work [--queue Q] [--concurrency N]
`);
return;
@@ -286,6 +287,49 @@ USAGE
break;
}
case 'smoke': {
const startTime = Date.now();
try { await queue.ensureSchema(); }
catch (e) {
console.error(`SMOKE FAIL — schema init: ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
}
const worker = new MinionWorker(engine, { queue: 'smoke', pollInterval: 100 });
worker.register('noop', async () => ({ ok: true, at: new Date().toISOString() }));
const job = await queue.add('noop', {}, { queue: 'smoke', max_attempts: 1 });
const workerPromise = worker.start();
const timeoutMs = 15000;
let final: MinionJob | null = null;
for (let elapsed = 0; elapsed < timeoutMs; elapsed += 100) {
await new Promise(r => setTimeout(r, 100));
final = await queue.getJob(job.id);
if (final && ['completed', 'failed', 'dead', 'cancelled'].includes(final.status)) break;
}
worker.stop();
await workerPromise;
const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(2);
if (final?.status === 'completed') {
const cfg = (await import('../core/config.ts')).loadConfig();
const engineLabel = cfg?.engine ?? 'unknown';
console.log(`SMOKE PASS — Minions healthy in ${elapsedSec}s (engine: ${engineLabel})`);
if (engineLabel === 'pglite') {
console.log('Note: the `gbrain jobs work` daemon requires Postgres. PGLite');
console.log('supports inline execution only (`submit --follow`).');
}
try { await queue.removeJob(job.id); } catch { /* non-fatal cleanup */ }
process.exit(0);
} else {
console.error(`SMOKE FAIL — job #${job.id} status: ${final?.status ?? 'timeout'} (${elapsedSec}s elapsed)`);
if (final?.error_text) console.error(` Error: ${final.error_text}`);
process.exit(1);
}
break;
}
case 'work': {
// Check if PGLite
const config = (await import('../core/config.ts')).loadConfig();