- **Sprint FT-RECURSIVE-002 (Phase 6b) — the recursive-retrain actuator (shipped default-off; live cycle next)** (2026-06-28). Makes the no-op `trigger_training_pipeline` real — a supervised controller that runs a gated retrain cycle — but **dormant behind `FT_LOOP_ENABLED=false`** (nothing trains or mutates serving state until opt-in). **Ledger:** `ft_training_cycles` (V0002, first writer) — event-sourced state machine (`triggered→curating→training→gating→promote_pending→{promoted|failed|rolled_back}`), DB-backed single-flight + retrain-interval gate that survive restarts. **Trigger gate** (`internal/ftloop`): a cycle launches only on Ready ∧ fresh-fraction (`FT_LOOP_MIN_FRESH_FRACTION` 0.30) ∧ no cycle within `FT_LOOP_MIN_RETRAIN_INTERVAL_HOURS` (168) — **and the SF-2 fix: when disabled/open/within-interval the trigger is SUPPRESSED** (no alert), ending the per-reflection-cycle `rsic-trigger_training_pipeline` spam (live-verified: 0 dispatches post-restart). **Compute lease** (single-host lockfile, reclaimable on expiry so a crashed trainer can't wedge RSIC) **+ `OrchestrationPolicy.Quiesce`** (pauses new RSIC triggers during a retrain) **+ disk-floor preflight.** **Controller:** supervised, walks curate→train→gate as ctx-cancellable Python subprocesses, ledger + `ft-loop:<stage>` jobhealth per stage, FAIL→archived+one alert, lease-expiry/disk-floor→class-4 alert-and-halt, PASS→`promote_pending` (halts — promotion is operator-gated via `mdemg ft-loop promote --cycle-id [--reject]`). `[AMD-7]` `TRAINING_READINESS_THRESHOLD` (+per-task) replaces the hardcoded 500; `[AMD-1]` epoch cap passed to train_ft. New config: `FT_LOOP_ENABLED`, `FT_LOOP_{MIN_RETRAIN_INTERVAL_HOURS,MIN_FRESH_FRACTION,POLL_INTERVAL_SEC,LEASE_MAX_HOURS,LEASE_PATH,MIN_FREE_DISK_GB,PYTHON_BIN,MODEL_VERSION}`, `FT_LORA_EPOCHS_CAP`, `FT_EARLY_STOP_VAL_LOSS_FACTOR`, `TRAINING_READINESS_THRESHOLD(_OVERRIDES)`. Unit tests across ledger/gate/lease/quiesce/controller; config guard 756/756; lint 0. **Epic 6 (2026-06-29/30):** every pipeline stage validated **live against the real system** — export (`mdemg data export`) → curate (`paradigm_router`, 4660-row SFT split) → train (real 168 MB LoRA adapter; loss 3.28→2.41) → convert (`fuse --dequantize`→`convert_hf_to_gguf`→`llama-quantize Q5_K_M`, 11 GB candidate) → gate (`run_benchmark` real calls, no-zero-call) — and the controller **wired with those proven commands** (`controller_stages.go`, PR #492). The orchestration (lease/quiesce/ledger/FAIL) was validated live in "Option A" (a real cycle walked `triggered→curating→failed`, SF-2 held, lease released). **A real bug was caught + fixed along the way:** the base-model SHA drift-guard had rotted (E6-8, PR #489 — upstream `mlx-community/Qwen3-14B-4bit` re-published its config; would've broken any retrain) and the quiesce design was confirmed necessary (E6-12: training saturated the box). 14 findings ledgered (`epic6_issues.md`). Remaining: a single *enabled* tiny-subset integration drill (the heavy convert + slow contended-box train) + Phase 7 (auto-promote/canary). Still **default-off** behind `FT_LOOP_ENABLED`. 12 Epic-6 config knobs (`FT_LOOP_{WORK_DIR,BASE_MODEL,BASE_SHA,UAITS_SPEC,BENCHMARK_CONFIG,LORA_RANK,LORA_ALPHA,GATE_PORT,EXPORT_SINCE_DAYS,GATE_TASK_FILTER,GATE_MIN_SCORE}`). Feature doc: `docs/features/ft-recursive-loop.md`. Sprint: `docs/development/ft-recursive-002/` (incl. `epic6_issues.md`, `run_record.md`).
0 commit comments