@@ -128,6 +128,12 @@ fn admitted_route<'a>(
128128 . unwrap_or_else ( || panic ! ( "strict canonical admission did not freeze route for `{id}`" ) )
129129}
130130
131+ fn task_runtime_id ( spec : & AgentSpec , task : & Task , host : & str ) -> String {
132+ task. id
133+ . clone ( )
134+ . unwrap_or_else ( || format ! ( "{}.{}" , spec. bus_id( host) , task. name) )
135+ }
136+
131137fn main_pty_id ( spec : & AgentSpec , host : & str ) -> Result < String > {
132138 let bus_id = spec. bus_id ( host) ;
133139 let main = spec
@@ -151,10 +157,7 @@ fn main_pty_id(spec: &AgentSpec, host: &str) -> Result<String> {
151157 "Agent Spec `{bus_id}` main PTY named `agent` must be an independently launchable service"
152158 ) ;
153159 }
154- let id = main
155- . id
156- . clone ( )
157- . unwrap_or_else ( || format ! ( "{bus_id}.agent" ) ) ;
160+ let id = task_runtime_id ( spec, main, host) ;
158161 if id. trim ( ) . is_empty ( ) {
159162 anyhow:: bail!( "Agent Spec `{bus_id}` main PTY id must be nonempty" ) ;
160163 }
@@ -211,7 +214,7 @@ fn load_canonical_eval_team(catalog: &Path, host: &str) -> Result<CanonicalEvalT
211214
212215 let mut paths = BTreeMap :: < PathBuf , usize > :: new ( ) ;
213216 let mut bus_ids = HashSet :: new ( ) ;
214- let mut main_ids = HashSet :: new ( ) ;
217+ let mut runtime_ids = BTreeMap :: < String , ( String , bool ) > :: new ( ) ;
215218 let mut seat_ids = Vec :: new ( ) ;
216219 let mut routes = BTreeMap :: new ( ) ;
217220 for spec in & found. specs {
@@ -254,8 +257,27 @@ fn load_canonical_eval_team(catalog: &Path, host: &str) -> Result<CanonicalEvalT
254257 }
255258 }
256259 let main_id = main_pty_id ( spec, host) ?;
257- if !main_ids. insert ( main_id. clone ( ) ) {
258- anyhow:: bail!( "canonical-agents found duplicate main PTY id `{main_id}`" ) ;
260+ for task in & spec. tasks {
261+ let runtime_id = task_runtime_id ( spec, task, host) ;
262+ if runtime_id. trim ( ) . is_empty ( ) {
263+ anyhow:: bail!(
264+ "canonical-agents Agent Spec `{bus_id}` task `{}` runtime task id must be nonempty" ,
265+ task. name
266+ ) ;
267+ }
268+ let is_main = task. kind == TaskKind :: Pty && task. name == "agent" ;
269+ if let Some ( ( previous, previous_is_main) ) =
270+ runtime_ids. insert ( runtime_id. clone ( ) , ( bus_id. clone ( ) , is_main) )
271+ {
272+ let kind = if is_main && previous_is_main {
273+ "duplicate main PTY runtime task id"
274+ } else {
275+ "duplicate runtime task id"
276+ } ;
277+ anyhow:: bail!(
278+ "canonical-agents found {kind} `{runtime_id}` in `{previous}` and `{bus_id}`"
279+ ) ;
280+ }
259281 }
260282 seat_ids. push ( main_id) ;
261283 let agent_dir = spec
@@ -519,15 +541,17 @@ fn from_is(from: Option<&str>, id: &str) -> bool {
519541
520542/// Wait for the DONE signal, message-driven (not grade-poll). Multi-seat teams require a
521543/// `sup → requester` confirmation whose timestamp follows a `worker → sup` report. A canonical
522- /// singleton instead requires its confirmation to post-date the exact kickoff receipt. Compact
523- /// singleton semantics remain unchanged. Bounded by `timeout`. Returns whether done fired.
544+ /// singleton instead requires a causally new requester-inbox entry at-or-after the exact kickoff
545+ /// receipt. Compact singleton semantics remain unchanged. Bounded by `timeout`. Returns whether done
546+ /// fired.
524547fn wait_done (
525548 bus : & Path ,
526549 canonical_routes : Option < & BTreeMap < String , CanonicalRoute > > ,
527550 sup : & str ,
528551 requester : & str ,
529552 workers : & [ String ] ,
530553 kickoff_ts : Option < u64 > ,
554+ requester_before_kickoff : Option < & HashSet < String > > ,
531555 timeout : Duration ,
532556 on_tick : & mut dyn FnMut ( ) ,
533557) -> bool {
@@ -559,11 +583,16 @@ fn wait_done(
559583 let sup_archived = crate :: message:: list_dir ( & sup_archive) . unwrap_or_default ( ) ;
560584 if workers. is_empty ( )
561585 && let Some ( kickoff_ts) = kickoff_ts
586+ && let Some ( before) = requester_before_kickoff
562587 {
563588 let confirmed = crate :: message:: list_dir ( & req_inbox)
564589 . unwrap_or_default ( )
565590 . iter ( )
566- . any ( |m| from_is ( m. from . as_deref ( ) , sup) && m. ts_ms > kickoff_ts) ;
591+ . any ( |m| {
592+ !before. contains ( & m. filename )
593+ && from_is ( m. from . as_deref ( ) , sup)
594+ && m. ts_ms >= kickoff_ts
595+ } ) ;
567596 if confirmed {
568597 return true ;
569598 }
@@ -1074,6 +1103,13 @@ fn run_eval_inner(spec: &Spec, eval: &Eval, spec_dir: &Path, catalog: &Path, hos
10741103 Some ( routes) => admitted_route ( routes, & sup) . inbox . clone ( ) ,
10751104 None => bus. join ( & sup) . join ( "inbox" ) ,
10761105 } ;
1106+ let requester_before_kickoff = eval. canonical_agents . then ( || {
1107+ crate :: message:: list_dir ( & bus. join ( & msg. from ) . join ( "inbox" ) )
1108+ . unwrap_or_default ( )
1109+ . into_iter ( )
1110+ . map ( |message| message. filename )
1111+ . collect :: < HashSet < _ > > ( )
1112+ } ) ;
10771113 let kickoff_receipt =
10781114 crate :: message:: send_to_inbox ( & to_inbox, & msg. from , None , None , & [ ] , & body)
10791115 . with_context ( || format ! ( "seeding kickoff into {}" , to_inbox. display( ) ) ) ?;
@@ -1185,6 +1221,7 @@ fn run_eval_inner(spec: &Spec, eval: &Eval, spec_dir: &Path, catalog: &Path, hos
11851221 & msg. from ,
11861222 & workers,
11871223 kickoff_ts,
1224+ requester_before_kickoff. as_ref ( ) ,
11881225 eval. max_timeout ,
11891226 & mut tick,
11901227 )
@@ -1559,6 +1596,28 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
15591596 identity "two"
15601597 host "evalhost"
15611598 pty "agent" { id "shared"; command "sleep 60" }
1599+ }"# ,
1600+ ) ,
1601+ ] ,
1602+ ) ,
1603+ (
1604+ "duplicate runtime task id" ,
1605+ vec ! [
1606+ (
1607+ "agents/evalhost/one/agent.kdl" ,
1608+ r#"agent "one" {
1609+ identity "one"
1610+ host "evalhost"
1611+ pty "agent" { id "shared"; command "sleep 60" }
1612+ }"# ,
1613+ ) ,
1614+ (
1615+ "agents/evalhost/two/agent.kdl" ,
1616+ r#"agent "two" {
1617+ identity "two"
1618+ host "evalhost"
1619+ pty "agent" { id "two-main"; command "sleep 60" }
1620+ exec "poison" { id "shared"; command "true" }
15621621}"# ,
15631622 ) ,
15641623 ] ,
@@ -1777,6 +1836,7 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
17771836 req,
17781837 & workers,
17791838 None ,
1839+ None ,
17801840 Duration :: from_millis( 100 ) ,
17811841 noop,
17821842 ) ) ;
@@ -1791,6 +1851,7 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
17911851 req,
17921852 & workers,
17931853 None ,
1854+ None ,
17941855 Duration :: from_millis( 100 ) ,
17951856 noop,
17961857 ) ) ;
@@ -1803,6 +1864,7 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
18031864 req,
18041865 & workers,
18051866 None ,
1867+ None ,
18061868 Duration :: from_millis( 2000 ) ,
18071869 noop,
18081870 ) ) ;
@@ -1830,6 +1892,7 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
18301892 req,
18311893 & workers,
18321894 None ,
1895+ None ,
18331896 Duration :: from_millis( 2000 ) ,
18341897 noop,
18351898 ) ,
@@ -1852,6 +1915,7 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
18521915 "req" ,
18531916 & [ "w" . to_string ( ) ] ,
18541917 None ,
1918+ None ,
18551919 Duration :: from_millis ( 400 ) ,
18561920 & mut tick,
18571921 ) ;
@@ -1860,7 +1924,7 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
18601924 }
18611925
18621926 #[ test]
1863- fn canonical_singleton_done_requires_a_confirmation_after_the_exact_kickoff ( ) {
1927+ fn canonical_singleton_done_is_new_after_kickoff_and_allows_the_same_millisecond ( ) {
18641928 let root = tempfile:: tempdir ( ) . unwrap ( ) ;
18651929 let agent_dir = root. path ( ) . join ( "agents/h/interviewer" ) ;
18661930 let routes = BTreeMap :: from ( [ (
@@ -1871,7 +1935,15 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
18711935 } ,
18721936 ) ] ) ;
18731937 let requester = root. path ( ) . join ( "requester/inbox" ) ;
1874- seed_msg ( & requester, 1_700_000_001_000 , "aaaaaa" , "h.interviewer" ) ;
1938+ let kickoff_ts = 1_700_000_002_000 ;
1939+ // An already-present message may even claim a FUTURE timestamp. Causality comes from the
1940+ // pre-kickoff filename snapshot, not wall-clock trust.
1941+ seed_msg ( & requester, kickoff_ts + 1_000 , "aaaaaa" , "h.interviewer" ) ;
1942+ let before = crate :: message:: list_dir ( & requester)
1943+ . unwrap ( )
1944+ . into_iter ( )
1945+ . map ( |message| message. filename )
1946+ . collect :: < HashSet < _ > > ( ) ;
18751947 let noop = & mut ( || { } ) as & mut dyn FnMut ( ) ;
18761948 assert ! (
18771949 !wait_done(
@@ -1880,25 +1952,28 @@ agent "worker" { identity "worker"; host "evalhost"; argv "true" }
18801952 "h.interviewer" ,
18811953 "requester" ,
18821954 & [ ] ,
1883- Some ( 1_700_000_002_000 ) ,
1955+ Some ( kickoff_ts) ,
1956+ Some ( & before) ,
18841957 Duration :: from_millis( 100 ) ,
18851958 noop,
18861959 ) ,
1887- "a pre-kickoff acknowledgement must not complete a singleton"
1960+ "a future-dated pre-kickoff acknowledgement must not complete a singleton"
18881961 ) ;
1889- seed_msg ( & requester, 1_700_000_003_000 , "bbbbbb" , "h.interviewer" ) ;
1962+ // Filename novelty establishes after-kickoff causality; `>=` keeps a valid same-ms reply.
1963+ seed_msg ( & requester, kickoff_ts, "bbbbbb" , "h.interviewer" ) ;
18901964 assert ! (
18911965 wait_done(
18921966 root. path( ) ,
18931967 Some ( & routes) ,
18941968 "h.interviewer" ,
18951969 "requester" ,
18961970 & [ ] ,
1897- Some ( 1_700_000_002_000 ) ,
1971+ Some ( kickoff_ts) ,
1972+ Some ( & before) ,
18981973 Duration :: from_secs( 1 ) ,
18991974 noop,
19001975 ) ,
1901- "a post-kickoff singleton confirmation should complete promptly"
1976+ "a newly appearing same-ms singleton confirmation should complete promptly"
19021977 ) ;
19031978 }
19041979
0 commit comments