3232
3333from ...contracts .v1 import SystemNotifyData
3434from ...kernel .actors import find_actor , list_actors
35- from ...kernel .group import Group , get_group_state , set_group_state
35+ from ...kernel .group import Group , get_group_state , load_group , set_group_state
3636from ...kernel .inbox import get_cursor , is_message_for_actor , set_cursor
3737from ...kernel .ledger import append_event
3838from ...kernel .system_prompt import render_system_prompt
@@ -89,6 +89,11 @@ def _get_auto_mark_on_delivery(group: Group) -> bool:
8989 return coerce_bool (delivery .get ("auto_mark_on_delivery" ), default = False )
9090
9191
92+ def should_auto_mark_on_delivery (group : Group ) -> bool :
93+ """Public helper for delivery-callers that need the current auto-mark policy."""
94+ return _get_auto_mark_on_delivery (group )
95+
96+
9297# ============================================================================
9398# State-aware Delivery Helpers
9499# ============================================================================
@@ -533,6 +538,18 @@ def debug_summary(self, group_id: str) -> Dict[str, Any]:
533538)
534539
535540
541+ def append_mcp_reply_reminder (text : str ) -> str :
542+ reminder = str (MCP_REMINDER_LINE or "" ).strip ()
543+ out = str (text or "" ).rstrip ("\n " )
544+ if not reminder :
545+ return out
546+ if reminder in out :
547+ return out
548+ if not out :
549+ return reminder
550+ return f"{ out } \n \n { reminder } "
551+
552+
536553def render_single_message (msg : PendingMessage ) -> str :
537554 """Render a single message for PTY delivery."""
538555 if msg .kind == "system.notify" :
@@ -568,6 +585,46 @@ def render_single_message(msg: PendingMessage) -> str:
568585 return f"{ head } :\n { body } " if "\n " in body else f"{ head } : { body } "
569586
570587
588+ def render_headless_control_text (* , control_kind : str , body : str ) -> str :
589+ kind = str (control_kind or "control" ).strip ().lower () or "control"
590+ content = str (body or "" ).strip ()
591+ if not content :
592+ return ""
593+ if kind == "bootstrap" :
594+ intro = (
595+ "[CCCC] INTERNAL CONTROL: session bootstrap\n "
596+ "Apply the following session operating instructions as authoritative for this session. "
597+ "Do not surface a reply, draft, or visible message for this bootstrap step. Wait for the next real work turn."
598+ )
599+ elif kind == "system_notify" :
600+ intro = (
601+ "[CCCC] INTERNAL CONTROL: system notification\n "
602+ "Treat the following as daemon-delivered coordination state for this actor. "
603+ "Do not emit a visible reply unless later work explicitly requires one."
604+ )
605+ else :
606+ intro = (
607+ "[CCCC] INTERNAL CONTROL\n "
608+ "Treat the following as daemon-delivered control-plane input for this session."
609+ )
610+ return f"{ intro } \n \n { content } " .strip ()
611+
612+
613+ def render_system_notify_delivery_text (* , notify : SystemNotifyData ) -> str :
614+ return render_single_message (
615+ PendingMessage (
616+ event_id = "" ,
617+ by = "system" ,
618+ to = [str (notify .target_actor_id or "" ).strip () or "@all" ],
619+ text = "" ,
620+ kind = "system.notify" ,
621+ notify_kind = str (notify .kind or "info" ),
622+ notify_title = str (notify .title or "" ),
623+ notify_message = str (notify .message or "" ),
624+ )
625+ )
626+
627+
571628def render_batched_messages (messages : List [PendingMessage ], * , reminder_after_index : Optional [int ] = None ) -> str :
572629 """Render multiple messages as a batch for PTY delivery."""
573630 if not messages :
@@ -580,10 +637,11 @@ def render_batched_messages(messages: List[PendingMessage], *, reminder_after_in
580637 for i , msg in enumerate (messages , 1 ):
581638 blocks .append (render_single_message (msg ))
582639
640+ out = "\n \n " .join ([b for b in blocks if b ]).rstrip ()
583641 if reminder_after_index is not None :
584- blocks . append ( MCP_REMINDER_LINE )
642+ out = append_mcp_reply_reminder ( out )
585643
586- return " \n \n " . join ([ b for b in blocks if b ]). rstrip ()
644+ return out
587645
588646
589647# ============================================================================
@@ -754,7 +812,7 @@ def deliver_message_with_preamble(
754812 delivered_before = THROTTLE .get_delivered_chat_count (group .group_id , aid )
755813 out = (message_text or "" ).rstrip ("\n " )
756814 if out and (delivered_before + 1 ) % REMINDER_EVERY_N_MESSAGES == 0 :
757- out = out + " \n \n " + MCP_REMINDER_LINE
815+ out = append_mcp_reply_reminder ( out )
758816 result = pty_submit_text (group , actor_id = aid , text = out )
759817 if result :
760818 THROTTLE .add_delivered_chat_count (group .group_id , aid , 1 )
@@ -856,11 +914,52 @@ def emit_system_notify(
856914 if not event_id :
857915 return event
858916
917+ headless_control_text = render_headless_control_text (
918+ control_kind = "system_notify" ,
919+ body = render_system_notify_delivery_text (notify = notify ),
920+ )
921+
859922 for aid in target_actor_ids :
860923 actor = find_actor (group , aid )
861924 if not isinstance (actor , dict ):
862925 continue
926+ runtime = str (actor .get ("runtime" ) or "" ).strip ().lower ()
863927 runner_kind = str (actor .get ("runner" ) or "pty" ).strip ()
928+ if runner_kind == "headless" and headless_control_text :
929+ delivered = False
930+ try :
931+ if runtime == "codex" :
932+ from ..codex_app_sessions import SUPERVISOR as codex_app_supervisor
933+
934+ if codex_app_supervisor .actor_running (group .group_id , aid ):
935+ delivered = bool (
936+ codex_app_supervisor .submit_control_message (
937+ group_id = group .group_id ,
938+ actor_id = aid ,
939+ text = headless_control_text ,
940+ control_kind = "system_notify" ,
941+ event_id = event_id ,
942+ ts = event_ts ,
943+ )
944+ )
945+ elif runtime == "claude" :
946+ from ..claude_app_sessions import SUPERVISOR as claude_app_supervisor
947+
948+ if claude_app_supervisor .actor_running (group .group_id , aid ):
949+ delivered = bool (
950+ claude_app_supervisor .submit_control_message (
951+ group_id = group .group_id ,
952+ actor_id = aid ,
953+ text = headless_control_text ,
954+ control_kind = "system_notify" ,
955+ event_id = event_id ,
956+ ts = event_ts ,
957+ )
958+ )
959+ except Exception :
960+ delivered = False
961+ if delivered :
962+ continue
864963 if runner_kind != "pty" :
865964 continue
866965 if not pty_runner .SUPERVISOR .actor_running (group .group_id , aid ):
@@ -957,6 +1056,28 @@ def maybe_auto_mark_delivered_event(
9571056 return False
9581057
9591058
1059+ def auto_mark_headless_delivery_started (
1060+ * ,
1061+ group_id : str ,
1062+ actor_id : str ,
1063+ event_id : str ,
1064+ ts : str ,
1065+ ) -> bool :
1066+ """Advance read state once a headless runtime has actually accepted a turn."""
1067+ gid = str (group_id or "" ).strip ()
1068+ if not gid :
1069+ return False
1070+ group = load_group (gid )
1071+ if group is None :
1072+ return False
1073+ return maybe_auto_mark_delivered_event (
1074+ group ,
1075+ actor_id = actor_id ,
1076+ event_id = event_id ,
1077+ ts = ts ,
1078+ )
1079+
1080+
9601081def _finish_delivery_chain (group : Group , * , actor_id : str ) -> None :
9611082 """Release delivery ownership and opportunistically continue queued work."""
9621083 gid = str (group .group_id or "" ).strip ()
0 commit comments