Skip to content

Commit 2df633e

Browse files
committed
Fix headless reply parity and runtime dock polish
1 parent 6fc78fe commit 2df633e

53 files changed

Lines changed: 3622 additions & 619 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/cccc/daemon/claude_app_sessions.py

Lines changed: 177 additions & 49 deletions
Large diffs are not rendered by default.

src/cccc/daemon/codex_app_sessions.py

Lines changed: 171 additions & 48 deletions
Large diffs are not rendered by default.

src/cccc/daemon/messaging/chat_ops.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from typing import Any, Callable, Dict, Optional
1010

11-
from ...contracts.v1 import ChatMessageData, ChatStreamData, DaemonError, DaemonResponse
11+
from ...contracts.v1 import ChatMessageData, ChatStreamData, DaemonError, DaemonResponse, SystemNotifyData
1212
from ...kernel.actors import find_actor, list_actors, resolve_recipient_tokens
1313
from ...kernel.group import get_group_state, load_group, set_group_state
1414
from ...kernel.inbox import find_event_with_chat_ack, is_message_for_actor
@@ -26,9 +26,11 @@
2626
from ..claude_app_sessions import SUPERVISOR as claude_app_supervisor
2727
from ..codex_app_sessions import SUPERVISOR as codex_app_supervisor
2828
from .delivery import (
29+
append_mcp_reply_reminder,
30+
emit_system_notify,
31+
should_auto_mark_on_delivery,
2932
flush_pending_messages,
3033
get_headless_targets_for_message,
31-
maybe_auto_mark_delivered_event,
3234
queue_chat_message,
3335
request_flush_pending_messages,
3436
)
@@ -270,21 +272,18 @@ def _notify_headless_targets(
270272
for actor_id in headless_targets:
271273
if actor_id in skip_ids:
272274
continue
273-
append_event(
274-
group.ledger_path,
275-
kind="system.notify",
276-
group_id=group.group_id,
277-
scope_key="",
275+
emit_system_notify(
276+
group,
278277
by="system",
279-
data={
280-
"kind": "info",
281-
"priority": notify_priority,
282-
"title": notify_title,
283-
"message": f"New message from {by}. Check your inbox.",
284-
"target_actor_id": actor_id,
285-
"requires_ack": False,
286-
"context": {"event_id": event_id, "from": by},
287-
},
278+
notify=SystemNotifyData(
279+
kind="info",
280+
priority=notify_priority,
281+
title=notify_title,
282+
message=f"New message from {by}. Check your inbox.",
283+
target_actor_id=actor_id,
284+
requires_ack=False,
285+
context={"event_id": event_id, "from": by},
286+
),
288287
)
289288
except Exception:
290289
pass
@@ -467,7 +466,9 @@ def handle_send(
467466
src_group_id=src_group_id,
468467
src_event_id=src_event_id,
469468
)
469+
headless_delivery_text = append_mcp_reply_reminder(delivery_text)
470470
actors = list_actors(group)
471+
auto_mark_on_delivery = should_auto_mark_on_delivery(group)
471472
skip_headless_notify_actor_ids: set[str] = set()
472473
logger.debug(f"[SEND] group={group_id} text={text[:30]!r} actors={[a.get('id') for a in actors]} effective_to={effective_to}")
473474
for actor in actors:
@@ -493,11 +494,12 @@ def handle_send(
493494
delivered = bool(codex_app_supervisor.submit_user_message(
494495
group_id=group.group_id,
495496
actor_id=actor_id,
496-
text=delivery_text,
497+
text=headless_delivery_text,
497498
event_id=event_id,
499+
ts=event_ts,
498500
attachments=attachments,
499501
))
500-
if delivered and maybe_auto_mark_delivered_event(group, actor_id=actor_id, event_id=event_id, ts=event_ts):
502+
if delivered and auto_mark_on_delivery:
501503
skip_headless_notify_actor_ids.add(actor_id)
502504
elif (
503505
runtime == "claude"
@@ -507,11 +509,12 @@ def handle_send(
507509
delivered = bool(claude_app_supervisor.submit_user_message(
508510
group_id=group.group_id,
509511
actor_id=actor_id,
510-
text=delivery_text,
512+
text=headless_delivery_text,
511513
event_id=event_id,
514+
ts=event_ts,
512515
attachments=attachments,
513516
))
514-
if delivered and maybe_auto_mark_delivered_event(group, actor_id=actor_id, event_id=event_id, ts=event_ts):
517+
if delivered and auto_mark_on_delivery:
515518
skip_headless_notify_actor_ids.add(actor_id)
516519
elif effective_runner_kind(runner_kind) == "pty":
517520
queue_chat_message(
@@ -725,6 +728,8 @@ def handle_reply(
725728
refs=refs,
726729
attachments=attachments,
727730
)
731+
headless_delivery_text = append_mcp_reply_reminder(delivery_text)
732+
auto_mark_on_delivery = should_auto_mark_on_delivery(group)
728733
skip_headless_notify_actor_ids: set[str] = set()
729734
for actor in list_actors(group):
730735
if not isinstance(actor, dict):
@@ -744,12 +749,13 @@ def handle_reply(
744749
delivered = bool(codex_app_supervisor.submit_user_message(
745750
group_id=group.group_id,
746751
actor_id=actor_id,
747-
text=delivery_text,
752+
text=headless_delivery_text,
748753
event_id=event_id,
754+
ts=event_ts,
749755
reply_to=target_event_id or reply_to,
750756
attachments=attachments,
751757
))
752-
if delivered and maybe_auto_mark_delivered_event(group, actor_id=actor_id, event_id=event_id, ts=event_ts):
758+
if delivered and auto_mark_on_delivery:
753759
skip_headless_notify_actor_ids.add(actor_id)
754760
elif (
755761
runtime == "claude"
@@ -759,12 +765,13 @@ def handle_reply(
759765
delivered = bool(claude_app_supervisor.submit_user_message(
760766
group_id=group.group_id,
761767
actor_id=actor_id,
762-
text=delivery_text,
768+
text=headless_delivery_text,
763769
event_id=event_id,
770+
ts=event_ts,
764771
reply_to=target_event_id or reply_to,
765772
attachments=attachments,
766773
))
767-
if delivered and maybe_auto_mark_delivered_event(group, actor_id=actor_id, event_id=event_id, ts=event_ts):
774+
if delivered and auto_mark_on_delivery:
768775
skip_headless_notify_actor_ids.add(actor_id)
769776
elif effective_runner_kind(runner_kind) == "pty":
770777
queue_chat_message(

src/cccc/daemon/messaging/delivery.py

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
from ...contracts.v1 import SystemNotifyData
3434
from ...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
3636
from ...kernel.inbox import get_cursor, is_message_for_actor, set_cursor
3737
from ...kernel.ledger import append_event
3838
from ...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+
536553
def 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+
571628
def 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+
9601081
def _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()

src/cccc/kernel/inbox.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,9 @@ def is_message_for_actor(
791791
# chat.message: check the "to" field
792792
targets = _message_targets(event)
793793

794-
# Internal actors observe normal chat routing but still require explicit
795-
# targeting for system.notify to avoid picking up background noise.
794+
if actor_internal:
795+
return actor_id in targets
796+
796797
# Empty targets = broadcast (everyone can see)
797798
if not targets:
798799
return True

src/cccc/kernel/system_prompt.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,6 @@ def render_role_system_prompt(
129129

130130
# Keep this stable and short. Long-lived playbook details belong in cccc_help.
131131
visible_reply_line = "- Visible replies must go through MCP: cccc_message_send / cccc_message_reply."
132-
runtime_lower = str(runtime_name or "").strip().lower()
133-
runner_lower = runner.lower()
134-
if runtime_lower == "codex" and runner_lower == "headless":
135-
visible_reply_line = (
136-
"- Do not call cccc_message_send / cccc_message_reply from codex headless; "
137-
"your final answer streams to Chat automatically."
138-
)
139132

140133
core_lines = [
141134
"Working Style:",

0 commit comments

Comments
 (0)