From 12429f70e7694eaec5dfe8570d30598113a90ae0 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 24 May 2026 00:32:09 -0700 Subject: [PATCH] =?UTF-8?q?v0.41:=20migration=20v93=20=E2=80=94=20minions?= =?UTF-8?q?=20audit=20tables=20+=20budget=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new audit tables for the v0.41 minions cathedral (each with SET NULL FK so audit rows survive `gbrain jobs prune`, denormalized context columns so post-NULL rows still carry forensic value): - minion_lease_pressure_log — Bug 2 audit (one row per lease-full bounce) - minion_budget_log — D5 audit (reserve/refund/spent/halted) - minion_self_fix_log — E6 audit (classifier-gated auto-resubmit chain) Three new columns on minion_jobs: - budget_remaining_cents — D5 parent spendable balance - budget_owner_job_id — Eng D7 immutable budget owner (FK SET NULL) - budget_root_owner_id — Eng D10 denormalized historical owner (no FK) Eng D10 closes the codex-pass-3 #4 ambiguity bug: when the budget owner is pruned mid-batch, `budget_owner_job_id` becomes NULL via SET NULL, which is indistinguishable from "never had a budget." The immutable `budget_root_owner_id` survives deletion so children can throw cleanly ("budget owner X deleted") instead of silently bypassing budget enforcement and becoming budget-free zombies. Audit table denormalization (codex pass-3 #7): queue_name, job_name, model, provider, root_owner_id persisted inline so "what model had pressure last Tuesday" queries still work after job pruning. Both Postgres + PGLite parity. Indexed for the read patterns the doctor check + jobs stats consume. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/migrate.ts | 87 ++++++++++++++++++++++++++ test/schema-bootstrap-coverage.test.ts | 12 ++++ 2 files changed, 99 insertions(+) diff --git a/src/core/migrate.ts b/src/core/migrate.ts index e337ade96..166079aea 100644 --- a/src/core/migrate.ts +++ b/src/core/migrate.ts @@ -4257,6 +4257,93 @@ export const MIGRATIONS: Migration[] = [ WHERE config ? 'github_repo'; `, }, + { + version: 93, + name: 'minions_v0_41_audit_and_budget', + // v0.41 minions cathedral — three audit tables + three new columns on + // minion_jobs. Single migration because the audit tables and budget + // columns are jointly designed and consumed: + // + // - minion_lease_pressure_log ← Bug 2 (releaseLeaseFullJob writes here) + // - minion_budget_log ← D5 (reservation / refund / halt / lost events) + // - minion_self_fix_log ← E6 (classifier-gated auto-resubmit chain) + // - minion_jobs.budget_remaining_cents ← D5 (parent spendable balance) + // - minion_jobs.budget_owner_job_id ← Eng D7 (immutable budget owner; FK SET NULL) + // - minion_jobs.budget_root_owner_id ← Eng D10 (denormalized historical + // owner, NO FK — persists past owner deletion so children can + // disambiguate "never had a budget" from "owner deleted, halt cleanly"). + // + // Audit table FKs are ON DELETE SET NULL (codex pass-2 #5) so audit rows + // survive `gbrain jobs prune`. Each audit table denormalizes context + // (queue_name, model, owner_id, event_type, etc.) at write time so + // post-NULL rows still carry forensic value — without denormalization + // they'd be timestamp-only residue (codex pass-3 #7). + // + // The retention sweep that bounds audit-table growth (Eng D8) lives in + // the autopilot cycle's `purge` phase, not here. This migration just + // creates the schema; the sweep ships in the same wave but is its own + // code path. + sql: ` + CREATE TABLE IF NOT EXISTS minion_lease_pressure_log ( + id BIGSERIAL PRIMARY KEY, + job_id BIGINT NULL REFERENCES minion_jobs(id) ON DELETE SET NULL, + lease_key TEXT NOT NULL, + active_at_bounce INTEGER NOT NULL, + max_concurrent INTEGER NOT NULL, + bounced_at TIMESTAMPTZ NOT NULL DEFAULT now(), + queue_name TEXT NULL, + job_name TEXT NULL, + model TEXT NULL, + provider TEXT NULL, + root_owner_id BIGINT NULL + ); + CREATE INDEX IF NOT EXISTS minion_lease_pressure_log_recent_idx + ON minion_lease_pressure_log (bounced_at DESC); + CREATE INDEX IF NOT EXISTS minion_lease_pressure_log_job_idx + ON minion_lease_pressure_log (job_id); + + CREATE TABLE IF NOT EXISTS minion_budget_log ( + id BIGSERIAL PRIMARY KEY, + job_id BIGINT NULL REFERENCES minion_jobs(id) ON DELETE SET NULL, + owner_id BIGINT NULL, + event_type TEXT NOT NULL, + cents_delta INTEGER NOT NULL, + turn_index INTEGER NULL, + model TEXT NULL, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS minion_budget_log_owner_idx + ON minion_budget_log (owner_id); + CREATE INDEX IF NOT EXISTS minion_budget_log_recent_idx + ON minion_budget_log (occurred_at DESC); + + CREATE TABLE IF NOT EXISTS minion_self_fix_log ( + id BIGSERIAL PRIMARY KEY, + parent_id BIGINT NULL REFERENCES minion_jobs(id) ON DELETE SET NULL, + child_id BIGINT NULL REFERENCES minion_jobs(id) ON DELETE SET NULL, + classifier_bucket TEXT NOT NULL, + chain_depth INTEGER NOT NULL, + policy_applied TEXT NULL, + outcome TEXT NULL, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS minion_self_fix_log_parent_idx + ON minion_self_fix_log (parent_id); + CREATE INDEX IF NOT EXISTS minion_self_fix_log_recent_idx + ON minion_self_fix_log (occurred_at DESC); + + ALTER TABLE minion_jobs ADD COLUMN IF NOT EXISTS budget_remaining_cents INTEGER NULL; + ALTER TABLE minion_jobs ADD COLUMN IF NOT EXISTS budget_owner_job_id BIGINT NULL + REFERENCES minion_jobs(id) ON DELETE SET NULL; + ALTER TABLE minion_jobs ADD COLUMN IF NOT EXISTS budget_root_owner_id BIGINT NULL; + CREATE INDEX IF NOT EXISTS minion_jobs_budget_owner_idx + ON minion_jobs (budget_owner_job_id) + WHERE budget_owner_job_id IS NOT NULL; + CREATE INDEX IF NOT EXISTS minion_jobs_budget_root_owner_idx + ON minion_jobs (budget_root_owner_id) + WHERE budget_root_owner_id IS NOT NULL; + `, + }, ]; export const LATEST_VERSION = MIGRATIONS.length > 0 diff --git a/test/schema-bootstrap-coverage.test.ts b/test/schema-bootstrap-coverage.test.ts index 5aeaeb5e8..2bf8eee99 100644 --- a/test/schema-bootstrap-coverage.test.ts +++ b/test/schema-bootstrap-coverage.test.ts @@ -719,6 +719,18 @@ const COLUMN_EXEMPTIONS = new Set([ // only via the eval-replay CLI, not via SQL filters that would force a // bootstrap probe. 'eval_candidates.schema_pack_per_source', + // v0.41 (migration v93) — minions cathedral budget columns. Same precedent + // as facts.claim_metric and friends: column-only additions on `minion_jobs`, + // no forward-reference index in PGLITE_SCHEMA_SQL (the partial indexes + // `minion_jobs_budget_owner_idx` + `minion_jobs_budget_root_owner_idx` + // live INSIDE the same v93 migration, not in the schema blob), and + // downstream callers explicitly handle NULL via the Eng D10 NULL-bypass + // branch in budget-tracker (jobs without `budget_owner_job_id` skip + // reservation entirely). Old brains pre-v93 silently get NULL on these + // columns; the budget enforcement path treats NULL as "no budget." + 'minion_jobs.budget_remaining_cents', + 'minion_jobs.budget_owner_job_id', + 'minion_jobs.budget_root_owner_id', ]); test('every ALTER TABLE ADD COLUMN in MIGRATIONS is covered by applyForwardReferenceBootstrap (column-only class)', async () => {