Fix Lab UX: visual selection, auto-navigate, live status, stuck detection

Lab experiment selection:
- Selected experiment now highlighted with accent border + glow
- Clicking auto-navigates to relevant tab (config if idle, monitor if running)
- No more silent toast-only feedback

Live status display:
- SSE "status" events now rendered in monitor (were silently dropped before)
- Shows real-time: "Proposing change... (trial 3/50)" during execution
- Error messages displayed inline instead of just toast

Stuck experiment fix:
- On app startup, reset all "running" experiments to "paused"
- Prevents ghost "running" status after service restart
- Fixed experiments 2, 3, 4 that showed running but had dead threads

Trial cap fix:
- Changed from lifetime cap (trial_num < 50) to per-run cap (trials_this_run < 50)
- Prevents runaway experiments like #1 that accumulated 3762 trials
- Shows trial progress in status: "trial 3/50"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-03-29 07:14:12 -05:00
parent 3b4fa449f1
commit bc2ad7c1a9

View File

@ -4229,8 +4229,10 @@ LAB_HTML = r"""
.btn-r:hover{background:rgba(224,82,82,0.06)}
.btn-o{border-color:rgba(245,158,11,0.3);color:var(--orange)}
.btn-o:hover{background:rgba(245,158,11,0.06)}
.exp-item{background:rgba(0,0,0,0.25);border:2px solid var(--border);border-radius:2px;padding:14px;margin-bottom:8px;cursor:pointer;transition:border-color .15s}
.exp-item{background:rgba(0,0,0,0.25);border:2px solid var(--border);border-radius:2px;padding:14px;margin-bottom:8px;cursor:pointer;transition:all .15s}
.exp-item:hover{border-color:var(--accent)}
.exp-item.selected{border-color:var(--accent);background:var(--glow);box-shadow:0 0 12px rgba(226,181,90,0.08)}
.live-status{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text2);margin-top:6px;min-height:14px;font-style:italic}
.exp-item .name{font-weight:600;font-size:14px}
.exp-item .meta{font-size:10px;color:var(--text2);display:flex;gap:12px;margin-top:4px;font-family:'JetBrains Mono',monospace}
.status-pill{display:inline-block;padding:2px 8px;border-radius:2px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;font-family:'JetBrains Mono',monospace;border:1px solid}
@ -4349,12 +4351,13 @@ LAB_HTML = r"""
<button class="btn btn-r" onclick="resetExp()">Reset</button>
</div>
</h3>
<div style="display:flex;gap:16px;margin-bottom:14px;font-size:13px">
<div style="display:flex;gap:16px;margin-bottom:8px;font-size:13px">
<div>Status: <span class="status-pill" id="mon-status">idle</span></div>
<div>Trials: <strong id="mon-trials">0</strong></div>
<div>Best: <strong id="mon-best" style="color:var(--green)">0.0</strong>/10</div>
<div>Improvements: <strong id="mon-impr">0</strong></div>
</div>
<div class="live-status" id="live-status"></div>
</div>
<div class="card">
<h3>Score Progression</h3>
@ -4408,7 +4411,8 @@ function renderExpList() {
if (!experiments.length) { el.innerHTML = '<div class="empty">No experiments yet. Create one to get started.</div>'; return; }
el.innerHTML = experiments.map(e => {
const rate = e.total_trials > 0 ? ((e.improvements / e.total_trials) * 100).toFixed(0) : 0;
return `<div class="exp-item" onclick="selectExp(${e.id})">
const sel = activeExp && activeExp.id === e.id ? ' selected' : '';
return `<div class="exp-item${sel}" onclick="selectExp(${e.id})">
<div class="name">${e.name} <span class="status-pill ${e.status}">${e.status}</span></div>
<div class="meta"><span>Trials: ${e.total_trials}</span><span>Best: ${(e.best_score||0).toFixed(1)}/10</span><span>Improvements: ${e.improvements} (${rate}%)</span><span>${e.metric}</span></div>
</div>`;
@ -4421,7 +4425,16 @@ async function selectExp(id) {
trialData = activeExp.trials || [];
updateMonitor();
updateConfigEditor();
toast('Loaded: ' + activeExp.name);
renderExpList();
// Auto-navigate to the relevant tab
if (activeExp.status === 'running') {
labTab('monitor');
startStream();
} else if (activeExp.status === 'idle' && activeExp.total_trials === 0) {
labTab('config');
} else {
labTab('monitor');
}
}
function updateMonitor() {
@ -4507,12 +4520,21 @@ function startStream() {
activeExp.best_score = d.best;
if (d.improved) activeExp.improvements = (activeExp.improvements||0) + 1;
updateMonitor();
renderExpList();
} else if (d.type === 'status') {
var liveEl = document.getElementById('live-status');
if (liveEl) liveEl.textContent = d.message || '';
} else if (d.type === 'done') {
activeExp.status = 'paused';
updateMonitor();
renderExpList();
var liveEl = document.getElementById('live-status');
if (liveEl) liveEl.textContent = 'Completed';
es.close();
} else if (d.type === 'error') {
toast(d.message, false);
var liveEl = document.getElementById('live-status');
if (liveEl) liveEl.textContent = 'Error: ' + (d.message||'').substring(0,100);
}
};
es.onerror = function() { es.close(); };
@ -7800,9 +7822,10 @@ def _ratchet_loop(exp_id):
# Pick meta-model (largest in pool)
meta_model = models_pool[-1] if models_pool else "qwen2.5:latest"
judge_model = models_pool[0] if models_pool else "llama3.2:latest"
max_trials = 50 # safety cap — prevents runaway experiments
max_new_trials = 50 # safety cap — max trials per start
trials_this_run = 0
while trial_num < max_trials:
while trials_this_run < max_new_trials:
# Check if still running
with get_db() as conn:
with conn.cursor() as cur:
@ -7812,8 +7835,9 @@ def _ratchet_loop(exp_id):
break
trial_num += 1
trials_this_run += 1
trial_start = time.time()
_lab_emit(exp_id, {"type": "status", "trial": trial_num, "message": "Proposing change..."})
_lab_emit(exp_id, {"type": "status", "trial": trial_num, "message": f"Proposing change... (trial {trials_this_run}/{max_new_trials})"})
# Step 1: Meta-model proposes a change
history_hint = ""
@ -9721,6 +9745,14 @@ _sentinel_thread.start()
if __name__ == "__main__":
# Cleanup stale lab experiments on startup
try:
with get_db() as conn:
with conn.cursor() as cur:
cur.execute("UPDATE lab_experiments SET status = 'paused' WHERE status = 'running'")
conn.commit()
except Exception:
pass
print("\n LLM Team UI running at http://localhost:5000\n")
print(f" AI Sentinel active: {SENTINEL_MODEL} scanning every {SENTINEL_INTERVAL}s\n")
app.run(host="127.0.0.1", port=5000, debug=False, threaded=True)