@@ -17,19 +17,22 @@ use challenge_common::{
1717} ;
1818use crypto:: KEY_LEN ;
1919use design_challenge_task:: { round_id_at, round_secs} ;
20- use design_http:: { mark_awaiting_admin, AdminAwardHook } ;
20+ use base64:: Engine ;
21+ use design_http:: { mark_awaiting_admin, schedule_harness_for_round, AdminAwardHook } ;
2122use design_prompts:: { prompt_set_digest, select_prompts_for_round} ;
2223use design_sandbox:: { SandboxBackend , SandboxError } ;
2324use design_sanitize:: sanitize_bundle;
2425use design_store:: {
2526 DesignStore , FinalScore , RatingRow , RoundRow , RunStage , StageEvent , StorePatch ,
2627} ;
2728use serde_json:: json;
29+ use sha2:: { Digest , Sha256 } ;
2830use submission_gating:: { GatingState , GatingStore } ;
2931use tokio:: time:: sleep;
3032use tracing:: { info, warn} ;
3133
3234use crate :: score:: { not_attempted, score_window, to_leaf, window_start, WindowScorePlan } ;
35+ use crate :: screenshot:: capture_full_page_png;
3336use crate :: CHALLENGE_ID ;
3437
3538/// Cap harness log payload stored in stage-event detail (JSON).
@@ -254,11 +257,62 @@ impl<C: ChainClient + Send + Sync + 'static> Orchestrator<C> {
254257 }
255258 }
256259 }
257- // Ensure current round row exists.
260+ // Ensure current round row exists and every active agent is queued .
258261 let _ = self . ensure_round ( rid) . await ;
262+ if let Err ( e) = self . schedule_active_for_round ( rid) . await {
263+ warn ! ( error = %e, round = rid, "schedule_active_for_round failed" ) ;
264+ }
259265 }
260266 }
261267
268+ /// Queue runs for every active (non-eliminated) harness in `rid`.
269+ ///
270+ /// Submit only schedules the *next* round once; this roll-forward keeps
271+ /// registered agents participating across the rolling 10-round window.
272+ async fn schedule_active_for_round ( & self , rid : u64 ) -> Result < ( ) , String > {
273+ let harnesses = self
274+ . store
275+ . list_active_harnesses ( rid)
276+ . await
277+ . map_err ( |e| e. to_string ( ) ) ?;
278+ let epoch = chain:: gather_schedule_state ( self . chain . as_ref ( ) , self . cfg . netuid )
279+ . map ( |s| chain:: current_epoch_pre_run_coinbase ( & s, s. current_block ) )
280+ . unwrap_or ( 0 ) ;
281+ for h in harnesses {
282+ match schedule_harness_for_round (
283+ self . store . as_ref ( ) ,
284+ & h,
285+ rid,
286+ self . cfg . netuid ,
287+ epoch,
288+ )
289+ . await
290+ {
291+ Ok ( ids) if !ids. is_empty ( ) => {
292+ info ! (
293+ harness_id = %h. id,
294+ miner = %h. miner_hotkey,
295+ round = rid,
296+ runs = ids. len( ) ,
297+ "scheduled active harness for round"
298+ ) ;
299+ }
300+ Ok ( _) => { }
301+ Err ( e) => {
302+ // Quota / elimination are expected; do not abort the loop.
303+ warn ! (
304+ harness_id = %h. id,
305+ miner = %h. miner_hotkey,
306+ round = rid,
307+ error = %e,
308+ "skip scheduling active harness"
309+ ) ;
310+ }
311+ }
312+ }
313+ Ok ( ( ) )
314+ }
315+
262316 /// Stuck sweeper.
263317 pub async fn run_sweeper ( self : Arc < Self > ) {
264318 loop {
@@ -705,7 +759,7 @@ impl<C: ChainClient + Send + Sync + 'static> Orchestrator<C> {
705759 // Sanitize reject is the miner's fault: terminal, no auto-retry.
706760 let sanitized = sanitize_bundle ( & out. pages )
707761 . map_err ( |e| RunFailure :: new ( ErrorClass :: Miner , e. to_string ( ) ) ) ?;
708- let pages: Vec < _ > = sanitized
762+ let mut pages: Vec < _ > = sanitized
709763 . pages
710764 . iter ( )
711765 . map ( |p| {
@@ -718,6 +772,29 @@ impl<C: ChainClient + Send + Sync + 'static> Orchestrator<C> {
718772 )
719773 } )
720774 . collect ( ) ;
775+ // Best-effort full-page screenshot of the styled index for the site UI
776+ // (replaces iframe previews). Failure never fails the run.
777+ if let Some ( index) = sanitized. pages . iter ( ) . find ( |p| p. path == "index.html" ) {
778+ let shot_dir = self . cfg . staging_root . join ( "screenshots" ) . join ( & run. id ) ;
779+ if let Some ( png) = capture_full_page_png ( & index. sanitized_html , & shot_dir) {
780+ let mut h = Sha256 :: new ( ) ;
781+ h. update ( & png) ;
782+ let sha = hex:: encode ( h. finalize ( ) ) ;
783+ let b64 = base64:: engine:: general_purpose:: STANDARD . encode ( & png) ;
784+ let bytes = u32:: try_from ( png. len ( ) ) . unwrap_or ( u32:: MAX ) ;
785+ pages. push ( (
786+ "index.png" . into ( ) ,
787+ b64,
788+ String :: new ( ) ,
789+ sha,
790+ bytes,
791+ ) ) ;
792+ info ! ( run_id = %run. id, bytes, "captured design page screenshot" ) ;
793+ } else {
794+ warn ! ( run_id = %run. id, "design page screenshot unavailable" ) ;
795+ }
796+ let _ = std:: fs:: remove_dir_all ( & shot_dir) ;
797+ }
721798 self . store
722799 . put_artifacts ( & run. id , & pages)
723800 . await
@@ -726,7 +803,9 @@ impl<C: ChainClient + Send + Sync + 'static> Orchestrator<C> {
726803
727804 // Pre-LLM copy gate: byte/AST copy of an *earlier* harness → terminal
728805 // `rejected` without spending the LLM review.
729- let gate_corpus = self . gate_corpus ( & run. harness_id ) . await ;
806+ let gate_corpus = self
807+ . gate_corpus ( & run. harness_id , & harness. miner_hotkey )
808+ . await ;
730809 if let Some ( hit) = copy_gate ( & harness. agent_py , harness. created_at_ms , & gate_corpus) {
731810 warn ! (
732811 run_id = %run. id,
@@ -857,14 +936,21 @@ impl<C: ChainClient + Send + Sync + 'static> Orchestrator<C> {
857936 Ok ( ( ) )
858937 }
859938
860- /// Corpus for the pre-LLM copy gate (recent harnesses minus the candidate).
861- async fn gate_corpus ( & self , exclude_harness_id : & str ) -> Vec < GateCorpusEntry > {
939+ /// Corpus for the pre-LLM copy gate (recent harnesses minus the candidate
940+ /// and any prior revisions from the same miner hotkey).
941+ async fn gate_corpus (
942+ & self ,
943+ exclude_harness_id : & str ,
944+ exclude_miner_hotkey : & str ,
945+ ) -> Vec < GateCorpusEntry > {
946+ let miner = exclude_miner_hotkey. to_ascii_lowercase ( ) ;
862947 self . store
863948 . list_recent_harnesses ( 64 )
864949 . await
865950 . unwrap_or_default ( )
866951 . into_iter ( )
867952 . filter ( |h| h. id != exclude_harness_id)
953+ . filter ( |h| h. miner_hotkey . to_ascii_lowercase ( ) != miner)
868954 . map ( |h| GateCorpusEntry {
869955 id : format ! ( "harness:{}" , h. id) ,
870956 source : h. agent_py ,
@@ -902,11 +988,11 @@ impl<C: ChainClient + Send + Sync + 'static> Orchestrator<C> {
902988 . list_recent_harnesses ( 64 )
903989 . await
904990 . map_err ( |e| RunFailure :: new ( ErrorClass :: AstInfra , e. to_string ( ) ) ) ?;
905- let cand_created = recent
906- . iter ( )
907- . find ( |h| h . id == run . harness_id )
908- . map ( |h| h. created_at_ms )
909- . unwrap_or ( 0 ) ;
991+ let cand = recent. iter ( ) . find ( |h| h . id == run . harness_id ) ;
992+ let cand_created = cand . map ( |h| h . created_at_ms ) . unwrap_or ( 0 ) ;
993+ let cand_miner = cand
994+ . map ( |h| h. miner_hotkey . to_ascii_lowercase ( ) )
995+ . unwrap_or_default ( ) ;
910996 let mut corpus: Vec < CorpusEntry > = vec ! [ CorpusEntry {
911997 // The published baseline is always in the corpus (same as prism):
912998 // it anchors originality judgments and keeps an empty recent-set
@@ -919,6 +1005,10 @@ impl<C: ChainClient + Send + Sync + 'static> Orchestrator<C> {
9191005 recent
9201006 . into_iter ( )
9211007 . filter ( |h| h. id != run. harness_id )
1008+ // Same-hotkey revisions are self-improvement, not copying.
1009+ . filter ( |h| {
1010+ cand_miner. is_empty ( ) || h. miner_hotkey . to_ascii_lowercase ( ) != cand_miner
1011+ } )
9221012 // Prior art only (created_at ordered like the pre-LLM gate): a
9231013 // later byte-copy must never poison the original's review.
9241014 . filter ( |h| {
0 commit comments