diff --git a/crates/challenge-agentic/src/tools.rs b/crates/challenge-agentic/src/tools.rs index 0c44973ff..cc14ff43c 100644 --- a/crates/challenge-agentic/src/tools.rs +++ b/crates/challenge-agentic/src/tools.rs @@ -704,8 +704,15 @@ mod tests { assert!(resolve_rel(&wd, "/etc/passwd").is_err()); } + /// `AGENTIC_ENABLE_RUN_COMMAND` is process-global; serialize the two tests + /// that mutate it so parallel libtest cannot race enable vs disable. + static RUN_COMMAND_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + #[test] fn run_command_executes_in_sandboxed_cwd() { + let _guard = RUN_COMMAND_ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); let dir = tempdir().unwrap(); fs::write(dir.path().join("agent.py"), "x = 1\n").unwrap(); std::env::set_var("AGENTIC_ENABLE_RUN_COMMAND", "1"); @@ -733,6 +740,10 @@ mod tests { #[test] fn run_command_disabled_without_container_env() { + let _guard = RUN_COMMAND_ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + std::env::remove_var("AGENTIC_ENABLE_RUN_COMMAND"); let dir = tempdir().unwrap(); fs::write(dir.path().join("agent.py"), "x = 1\n").unwrap(); let req = ReviewRequest { diff --git a/crates/design-http/src/stats.rs b/crates/design-http/src/stats.rs index 2024cb948..bf5913ed0 100644 --- a/crates/design-http/src/stats.rs +++ b/crates/design-http/src/stats.rs @@ -102,11 +102,64 @@ pub async fn get_stats(State(st): State>) -> Response { .into_response() } +/// Cap for dashboard `recent_runs` (site gallery + activity feed). +const RECENT_RUNS_LIMIT: usize = 40; +/// Wider scan so a queue flood of brand-new rows cannot hide screenshot runs. +const RECENT_RUNS_SCAN: u32 = 500; + +/// Stages that normally carry a captured `index.png` after sanitize. +fn likely_has_screenshot(status: RunStage) -> bool { + matches!( + status, + RunStage::AgenticReview + | RunStage::AwaitingAdmin + | RunStage::AwaitingAnnotation + | RunStage::Scored + ) +} + +/// Prefer post-sanitize / scored runs (gallery candidates), then fill with the +/// newest remaining rows so the feed still reflects live queue activity. +fn select_recent_runs(runs: &[design_store::RunState], limit: usize) -> Vec { + let mut viewable = Vec::new(); + let mut other = Vec::new(); + for r in runs { + if likely_has_screenshot(r.status) { + viewable.push(r); + } else { + other.push(r); + } + } + viewable + .into_iter() + .chain(other) + .take(limit) + .map(|r| { + let (prompt_title, _) = prompt_fields(&r.prompt_id); + json!({ + "id": r.id, + "status": r.status.as_str(), + "round_id": r.round_id, + "harness_id": r.harness_id, + "prompt_id": r.prompt_id, + "prompt_title": prompt_title, + "error_detail": r.error_detail, + "created_at_ms": r.created_at_ms, + "updated_at_ms": r.updated_at_ms, + }) + }) + .collect() +} + /// `GET /v1/dashboard` — one-shot UI payload (status + leaderboard + recent jobs). pub async fn get_dashboard(State(st): State>) -> Response { let secs = now_secs(); let rid = round_id_at(secs); - let runs = st.store.list_runs(None, 100).await.unwrap_or_default(); + let runs = st + .store + .list_runs(None, RECENT_RUNS_SCAN) + .await + .unwrap_or_default(); let by_status = count_by_status(&runs); let round = st.store.get_round(rid).await.ok().flatten(); let ratings = st.store.ratings_for_round(rid).await.unwrap_or_default(); @@ -120,24 +173,7 @@ pub async fn get_dashboard(State(st): State>) -> Response { vec![] }; let opens = rid * round_secs(); - let jobs: Vec = runs - .iter() - .take(40) - .map(|r| { - let (prompt_title, _) = prompt_fields(&r.prompt_id); - json!({ - "id": r.id, - "status": r.status.as_str(), - "round_id": r.round_id, - "harness_id": r.harness_id, - "prompt_id": r.prompt_id, - "prompt_title": prompt_title, - "error_detail": r.error_detail, - "created_at_ms": r.created_at_ms, - "updated_at_ms": r.updated_at_ms, - }) - }) - .collect(); + let jobs = select_recent_runs(&runs, RECENT_RUNS_LIMIT); let lb = |rows: &[design_store::RatingRow]| -> Vec { rows.iter() .map(|r| { @@ -315,3 +351,49 @@ pub async fn run_status_json(store: &dyn DesignStore, id: &str) -> Result RunState { + RunState { + id: id.into(), + round_id: 1, + harness_id: "h".into(), + prompt_id: "p01".into(), + status, + artifact_digest: None, + sanitize_report: None, + agentic_verdict: None, + error_detail: None, + final_score: None, + retry_count: 0, + created_at_ms, + updated_at_ms: created_at_ms, + } + } + + #[test] + fn select_recent_runs_prefers_screenshot_stages() { + // Newest-first scan: a flood of queued rows would otherwise fill the + // site gallery window and hide the awaiting_admin candidate. + let runs = vec![ + run("q1", RunStage::Queued, 100), + run("q2", RunStage::Queued, 99), + run("q3", RunStage::Queued, 98), + run("a1", RunStage::AwaitingAdmin, 97), + run("s1", RunStage::Scored, 96), + ]; + let jobs = select_recent_runs(&runs, 3); + let ids: Vec<&str> = jobs + .iter() + .filter_map(|j| j.get("id").and_then(|v| v.as_str())) + .collect(); + assert_eq!(ids, vec!["a1", "s1", "q1"]); + } +} diff --git a/crates/site-api/src/handlers.rs b/crates/site-api/src/handlers.rs index d6eb4019c..753a6b622 100644 --- a/crates/site-api/src/handlers.rs +++ b/crates/site-api/src/handlers.rs @@ -308,12 +308,25 @@ async fn design_leaderboard_json( .and_then(Value::as_array) .cloned() .unwrap_or_default(); + let previous_round = dash + .as_ref() + .and_then(|d| d.pointer("/leaderboard/previous_round")) + .and_then(Value::as_u64) + .or_else(|| round_id.map(|r| r.saturating_sub(1))); let epoch = dash .as_ref() .and_then(|d| d.get("epoch")) .and_then(Value::as_u64) .unwrap_or(0); - let mut rows = design_leaderboard(&ratings, &previous, epoch); + // Mid-round before admin winners: current ratings are empty. Surface the + // previous round's standings so the marketing board is not blank. + let (board_ratings, board_previous, board_round_id) = + if ratings.is_empty() && !previous.is_empty() { + (previous.as_slice(), &[][..], previous_round) + } else { + (ratings.as_slice(), previous.as_slice(), round_id) + }; + let mut rows = design_leaderboard(board_ratings, board_previous, epoch); if let Some(needle) = q.filter(|s| !s.trim().is_empty()) { rows.retain(|r| leaderboard_matches_query(r, needle)); } @@ -334,7 +347,7 @@ async fn design_leaderboard_json( "total": page_out.total, "pageCount": page_out.page_count, "epoch": epoch, - "roundId": round_id, + "roundId": board_round_id, "roundEndsAt": round_ends_at, "secondsRemaining": seconds_remaining, "updatedAt": now_iso(), @@ -1028,6 +1041,39 @@ mod tests { ); } + #[tokio::test] + async fn design_leaderboard_falls_back_to_previous_round() { + let (design, _prism, st) = setup().await; + Mock::given(method("GET")) + .and(path("/v1/dashboard")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "epoch": 3, + "leaderboard": { + "current_round": 9, + "previous_round": 8, + "ratings": [], + "previous_ratings": [ + {"miner_hotkey": "aa".repeat(32), "rating": 250, "wins": 1, "losses": 0} + ] + }, + "round": { + "round_id": 9, + "closes_at_secs": 1_700_000_100_u64, + "seconds_remaining": 120 + }, + "recent_runs": [] + }))) + .mount(&design) + .await; + let app = site_router(st); + + let (s, v) = call(app, "/v1/site/arenas/design/leaderboard").await; + assert_eq!(s, StatusCode::OK, "{v}"); + assert_eq!(v["total"], 1, "{v}"); + assert_eq!(v["roundId"], 8, "{v}"); + assert_eq!(v["items"][0]["elo"], 250.0); + } + #[tokio::test] async fn arenas_carry_trust_root_shares_and_weights_endpoint() { use std::sync::Arc; diff --git a/docs/SITE_API.md b/docs/SITE_API.md index 8df6d5f7a..48a0173e3 100644 --- a/docs/SITE_API.md +++ b/docs/SITE_API.md @@ -21,10 +21,16 @@ public preview is `screenshotUrl` → `/challenge/design/v1/view/{runId}/index.p `https://chain.joinbase.ai/challenge/design/v1/view/{runId}/index.png`) so PNG bytes are not proxied through the site's Vercel `/gbase-api` rewrite. JSON `/v1/site/*` calls may keep using the same-origin proxy. Runs without a captured -screenshot are excluded from the submissions list. -Leaderboard `elo` is the design -`rating` field. Prism window series use real terminal `bpb` with a single -`[final]` point when no step curve is stored. +screenshot are excluded from the submissions list. Design `GET /v1/dashboard` +`recent_runs` therefore prioritizes post-sanitize stages (`awaiting_admin`, +`scored`, …) over a flood of brand-new `queued` rows so the site gallery is not +starved. + +Leaderboard `elo` is the design `rating` field. When the current round has no +winners yet (`ratings: []`), `/v1/site/arenas/design/leaderboard` surfaces the +previous round's standings (`roundId` = previous) rather than an empty board. +Prism window series use real terminal `bpb` with a single `[final]` point when +no step curve is stored. `GET /v1/site/arenas/{slug}/submissions` and `/leaderboard` accept optional `?q=` — case-insensitive substring over miner hotkey (SS58 or hex), handle,