@@ -344,6 +344,13 @@ def collect_background_results() -> list[str]:
344344 return notifications
345345
346346
347+ def has_pending_background () -> bool :
348+ """Non-destructive: True if any background task has completed and is
349+ waiting to be collected. The inbox poller uses this in its wake condition."""
350+ with background_lock :
351+ return any (t ["status" ] == "completed" for t in background_tasks .values ())
352+
353+
347354# ── Cron Scheduler (from s14, synced) ──
348355
349356DURABLE_PATH = WORKDIR / ".scheduled_tasks.json"
@@ -911,7 +918,8 @@ def agent_loop(messages: list, context: dict):
911918 history = []
912919 context = update_context ({}, [])
913920
914- # input() and a 1s inbox poller feed one event queue (issue #291).
921+ # input() and a 1s poller (teammate inbox or background results) feed one
922+ # event queue (issues #291, #46).
915923 events = queue .Queue ()
916924
917925 def input_reader ():
@@ -924,17 +932,19 @@ def input_reader():
924932 events .put (("user" , line ))
925933
926934 def inbox_poller ():
927- # Poll ~1s and submit the Lead's inbox as a new turn. Don't gate on
935+ # Poll ~1s and wake the Lead when async results are ready: teammate
936+ # inbox messages or completed background tasks. Don't gate on
928937 # active_teammates: a teammate sends its result and then removes itself,
929938 # so the final message can outlive its registry entry.
930939 while True :
931940 time .sleep (1 )
932- if BUS .peek ("lead" ):
933- events .put (("inbox " , None ))
941+ if BUS .peek ("lead" ) or has_pending_background () :
942+ events .put (("wake " , None ))
934943
935944 threading .Thread (target = input_reader , daemon = True ).start ()
936945 threading .Thread (target = inbox_poller , daemon = True ).start ()
937946
947+ had_teammates = False
938948 while True :
939949 kind , payload = events .get ()
940950 if kind == "quit" :
@@ -943,20 +953,31 @@ def inbox_poller():
943953 if payload .strip ().lower () in ("q" , "exit" , "" ):
944954 break
945955 history .append ({"role" : "user" , "content" : payload })
946- else : # "inbox": a teammate message woke the Lead
956+ else : # "wake": teammate inbox or background results are ready
957+ parts = []
947958 inbox = BUS .read_inbox ("lead" )
948- if not inbox :
959+ if inbox :
960+ parts .append ("[Inbox]\n " + "\n " .join (
961+ f"From { m ['from' ]} : { m ['content' ][:200 ]} " for m in inbox ))
962+ bg = collect_background_results ()
963+ parts .extend (bg )
964+ if not parts :
949965 continue # already drained by an earlier wake (idempotent)
950- inbox_text = "\n " .join (
951- f"From { m ['from' ]} : { m ['content' ][:200 ]} " for m in inbox )
952- history .append ({"role" : "user" ,
953- "content" : f"[Inbox]\n { inbox_text } " })
954- print (f"\n \033 [33m[Inbox: { len (inbox )} messages → new turn]\033 [0m" )
966+ history .append ({"role" : "user" , "content" : "\n " .join (parts )})
967+ print (f"\n \033 [33m[wake: { len (inbox )} inbox + { len (bg )} background "
968+ f"-> new turn]\033 [0m" )
955969
956970 # One turn for whichever source woke us.
957971 agent_loop (history , context )
958972 context = update_context (context , history )
959973 for block in history [- 1 ]["content" ]:
960974 if getattr (block , "type" , None ) == "text" :
961975 print (block .text )
976+
977+ # Announce once when every teammate has finished and its output drained.
978+ if active_teammates :
979+ had_teammates = True
980+ elif had_teammates and not BUS .peek ("lead" ) and not has_pending_background ():
981+ print ("\033 [32m[all teammates done]\033 [0m" )
982+ had_teammates = False
962983 print ()
0 commit comments