Skip to content

Story A — Interior Loop (autoresearch engine) [owner: Me] #2

Description

@PlayerPlanet

Owner: Me (lead) · Track: Aiven clean-room (front-runner) · Implemented by: Claude Code + dev-team plugin

Goal

A working autoresearch loop that proposes a DB change, applies it to a live Postgres, measures it on a frozen benchmark, and keeps-or-rolls-back — producing one clean descending p99 curve on one task.

⚠️ Gate-1 update (verified 2026-06-24 on konsta-sunsteadhack / service sunstead-pg-bench, RUNNING).

  • hypopg is NOT available on our Aiven PG plan (0 of 60 extensions; no pg_hint_plan either). The original "no-build hypothetical index" path is closed.
  • Substitute (now the plan): actions.apply runs a real CREATE INDEX, actions.rollback a DROP INDEX. Keep the dataset small (pgbench scale ~10–50, or a trimmed JOB) so each build is sub-second-to-seconds. This is fully reversible and more honest than a cost-only proxy — you measure the real index, not an estimate.
  • Available & useful: pg_stat_statements (1.11) ✅, pg_stat_monitor (2.1, native p99 percentile buckets) ✅, pg_prewarm/pg_buffercache (warmup control) ✅, pg_repack/pg_cron ✅.

You build

  • cleanroom/loop/ — the dependency-injected loop + keep/discard/rollback control flow
  • cleanroom/actions/ — apply + rollback adapters (start with index discovery: real CREATE INDEX / DROP INDEX on a small dataset — see Gate-1 note; reversible, no hypopg)
  • the default Claude proposer

Do not edit files outside cleanroom/loop/ and cleanroom/actions/. Consume B's modules and C's proposers only through the frozen interfaces below.

Tasks, in order (the "when")

  1. Phase 0 — Implement run_loop against the fixture stubs in cleanroom/fixtures/ (canned benchmark, no-op pore, in-memory logclient). Do not wait for B. Control flow: propose → pore.evaluate → actions.apply → benchmark.run → is_within_noise gate → keep/discard → logclient.write_experiment (+ rollback on regression).
  2. Phase 0 — Implement actions/ for task 1 = index discovery with apply/rollback. ⚠️ hypopg is unavailable on our Aiven plan (verified 2026-06-24 — see Gate-1 note): apply does a real CREATE INDEX, rollback a DROP INDEX. Keep the dataset small (pgbench scale ~10–50 or trimmed JOB) so build time stays sub-second-to-seconds — reversible, and more honest than a cost-only proxy.
  3. Phase 1 — Swap fixtures for B's real benchmark/pore/logclient (integration point test #1). Run on one frozen workload against sunstead-pg-bench (or a throwaway Postgres) → produce the first descending p99 curve.
  4. Phase 2 — Add task 2 (query-rewrite — MUST call check_correctness) and task 3 (GUC tuning — apply live if dynamic, flag restart-required).
  5. Expose run_loop's proposer parameter so C can inject alternate models (integration point with the model axis).

Async strategy

The fixtures/ stubs let you build and demo the entire loop before B lands. Fall back to a local throwaway Postgres if Aiven access is delayed.

Definition of done

run_loop on the index task produces a descending p99 series written to the experiment table, rolls back on regression, and the significance gate rejects within-variance changes.

Method

Lead with dev-team-planner to design the loop against the contracts below, then dev-team-builder + dev-team-tester.


Frozen contracts (shared across issues #A/#B/#C — implement against these, do not change unilaterally)

Postgres log schema (source of truth)

create table experiment (
  id bigserial primary key,
  task_id text not null,
  model text not null,
  drift_level double precision default 0,
  candidate jsonb not null,              -- {type, params, reversible}
  baseline_p99 double precision, candidate_p99 double precision,
  cost_estimate double precision,
  correctness_ok boolean, within_noise boolean,
  decision text not null,                -- 'keep'|'discard'|'rollback'|'escalated'
  created_at timestamptz default now()
);
create table crossing (
  id bigserial primary key, experiment_id bigint references experiment(id),
  pore text not null, risk_level text not null,
  requires_human_judgment boolean not null, action jsonb not null,
  created_at timestamptz default now()
);
create table judgment (
  id bigserial primary key, crossing_id bigint references crossing(id),
  judge text not null, judge_kind text not null,   -- rule|human|agent
  decision text not null, rationale text,           -- approve|reject|escalate
  created_at timestamptz default now()
);

Interfaces (signatures are the contract)

# proposer (A owns default; C swaps per-model)
propose(task_spec, history) -> Candidate{type, params, reversible}
# benchmark (B owns)
run_benchmark(conn, workload_id, *, warmup, trials) -> Result{p99_ms, throughput, cost_estimate, samples}
check_correctness(conn, candidate) -> bool
is_within_noise(baseline_samples, candidate_samples) -> bool
# pore (B owns — pure function, frozen rules)
evaluate(candidate) -> PoreResult{pore, risk_level, requires_human_judgment, decision}
# logclient (B owns — thin lib used by all)
write_experiment(...) -> id ; write_crossing(...) -> id ; write_judgment(...) ; read_experiments(filter) -> rows
# loop (A owns — pure dependency injection)
run_loop(task_spec, *, proposer, benchmark, pore, logclient, iterations)

Integration timeline

Phase 0:  B ships schema+stubs → A builds on stubs, C builds on synthetic log   (all parallel)
Phase 1:  A ⟵ B's real harness → first descending curve            (integration #1)
Phase 2:  A adds tasks + B's pore fires escalations into the log
Phase 3:  C ⟵ A's real log + injects proposers → boundary curve + model axis (integration #2/#3)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions