v0.41: migration v93 — minions audit tables + budget columns

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) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-24 00:32:09 -07:00
co-authored by Claude Opus 4.7
parent 677142a680
commit 12429f70e7
2 changed files with 99 additions and 0 deletions
+87
View File
@@ -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
+12
View File
@@ -719,6 +719,18 @@ const COLUMN_EXEMPTIONS = new Set<string>([
// 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 () => {