From 4e8a7671b0ae5b73062070a0b6134e47648bc2d8 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 18 Apr 2026 07:31:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Minions=20adoption=20UX=20=E2=80=94=20s?= =?UTF-8?q?moke=20test=20+=20migration=20+=20pain-triggered=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 2 +- README.md | 3 +- TODOS.md | 4 +- skills/conventions/subagent-routing.md | 99 ++++++++++++--- skills/migrations/v0.11.0.md | 162 +++++++++++++++++++++++++ src/commands/jobs.ts | 44 +++++++ 6 files changed, 296 insertions(+), 18 deletions(-) create mode 100644 skills/migrations/v0.11.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d170be6..0361e8133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 8fe6b6a8e..7e763d101 100644 --- a/README.md +++ b/README.md @@ -328,12 +328,13 @@ EMBEDDINGS LINKS + GRAPH gbrain link|unlink|backlinks|graph Cross-reference management -JOBS (Minions v7) +JOBS (Minions) gbrain jobs submit [--params JSON] [--follow] Submit a background job gbrain jobs list [--status S] [--queue Q] List jobs with filters gbrain jobs get|cancel|retry|delete 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 diff --git a/TODOS.md b/TODOS.md index cec21a512..c3033602f 100644 --- a/TODOS.md +++ b/TODOS.md @@ -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 * * * *' } })`. diff --git a/skills/conventions/subagent-routing.md b/skills/conventions/subagent-routing.md index 576f1e736..2166aefb6 100644 --- a/skills/conventions/subagent-routing.md +++ b/skills/conventions/subagent-routing.md @@ -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. diff --git a/skills/migrations/v0.11.0.md b/skills/migrations/v0.11.0.md new file mode 100644 index 000000000..45880ea10 --- /dev/null +++ b/skills/migrations/v0.11.0.md @@ -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 </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`." diff --git a/src/commands/jobs.ts b/src/commands/jobs.ts index fbb6d4d88..3f6447e73 100644 --- a/src/commands/jobs.ts +++ b/src/commands/jobs.ts @@ -66,6 +66,7 @@ USAGE gbrain jobs prune [--older-than 30d] gbrain jobs delete 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();