mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
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:
co-authored by
Claude Opus 4.7
parent
78eea2e3eb
commit
4e8a7671b0
+1
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 * * * *' } })`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`."
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user