Skip to content

Commit e9eb5b8

Browse files
committed
Merge commit 'ffda3f8738490e2128982bef7972a52d6707fc37'
# Conflicts: # web/src/components/layout/AppHeader.tsx # web/src/components/layout/MobileMenuSheet.tsx
2 parents 3baf05b + ffda3f8 commit e9eb5b8

145 files changed

Lines changed: 14825 additions & 2598 deletions

File tree

Some content is hidden

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

src/cccc/cli/common.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import argparse
44
import json
55
import os
6+
import shutil
67
import signal
78
import socket
89
import subprocess
@@ -124,6 +125,36 @@ def _cleanup_daemon_state_files(home: Path) -> None:
124125
path.unlink(missing_ok=True)
125126

126127

128+
def _same_home_daemon_lock_holder_pids(home: Path) -> list[int]:
129+
if os.name != "posix":
130+
return []
131+
if not shutil.which("lsof"):
132+
return []
133+
lock_path = home / "daemon" / "ccccd.lock"
134+
try:
135+
proc = subprocess.run(
136+
["lsof", "-t", str(lock_path)],
137+
capture_output=True,
138+
text=True,
139+
check=False,
140+
)
141+
except Exception:
142+
return []
143+
if int(proc.returncode or 0) not in {0, 1}:
144+
return []
145+
146+
pids: list[int] = []
147+
for line in str(proc.stdout or "").splitlines():
148+
text = str(line or "").strip()
149+
if not text.isdigit():
150+
continue
151+
pid = int(text)
152+
if pid <= 0 or pid == os.getpid():
153+
continue
154+
pids.append(pid)
155+
return sorted(set(pids))
156+
157+
127158
def _same_home_daemon_pids(home: Path) -> list[int]:
128159
if os.name != "posix":
129160
return []
@@ -132,7 +163,7 @@ def _same_home_daemon_pids(home: Path) -> list[int]:
132163
default_home = str(ensure_home().resolve())
133164
proc_root = Path("/proc")
134165
if not proc_root.is_dir():
135-
return []
166+
return _same_home_daemon_lock_holder_pids(home)
136167
pids: list[int] = []
137168
try:
138169
proc_dirs = list(proc_root.iterdir())

src/cccc/contracts/v1/group_space.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ class SpaceJob(BaseModel):
6767
remote_space_id: str = ""
6868
kind: SpaceJobKind = "context_sync"
6969
payload: Dict[str, Any] = Field(default_factory=dict)
70+
payload_ref: str = ""
7071
result: Dict[str, Any] = Field(default_factory=dict)
7172
payload_digest: str = ""
73+
payload_bytes: int = 0
7274
idempotency_key: str = ""
7375
state: SpaceJobState = "pending"
7476
attempt: int = 0

src/cccc/contracts/v1/message.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class ChatMessageData(BaseModel):
5050
source_user_name: Optional[str] = None # External IM sender display name
5151
source_user_id: Optional[str] = None # External IM sender platform user id
5252
mention_user_ids: Optional[List[str]] = None # External IM real-mention targets
53+
sender_title: Optional[str] = None # Immutable sender title snapshot for message rendering
54+
sender_runtime: Optional[str] = None # Immutable sender runtime snapshot for message rendering
55+
sender_avatar_path: Optional[str] = None # Immutable blob-backed sender avatar path
5356

5457
# Cross-group provenance (for relays/forwarding)
5558
src_group_id: Optional[str] = None
@@ -68,6 +71,7 @@ class ChatMessageData(BaseModel):
6871

6972
# Streaming
7073
stream_id: Optional[str] = None # Links final message to its stream
74+
pending_event_id: Optional[str] = None # Stable turn-scoped id to reconcile transient UI bubbles
7175

7276
# Metadata
7377
client_id: Optional[str] = None # Client-generated idempotency key
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
import threading
4+
from typing import Any, Dict
5+
6+
_LOCK = threading.Lock()
7+
_BY_GROUP: Dict[str, Dict[str, Dict[str, Any]]] = {}
8+
9+
10+
def replace_group_runtime(group_id: str, actors: Dict[str, Dict[str, Any]]) -> None:
11+
gid = str(group_id or "").strip()
12+
if not gid:
13+
return
14+
normalized: Dict[str, Dict[str, Any]] = {}
15+
for actor_id, payload in actors.items():
16+
aid = str(actor_id or "").strip()
17+
if not aid or not isinstance(payload, dict):
18+
continue
19+
normalized[aid] = {
20+
"running": bool(payload.get("running")),
21+
"runner_effective": str(payload.get("runner_effective") or "").strip() or "pty",
22+
"idle_seconds": payload.get("idle_seconds"),
23+
"effective_working_state": str(payload.get("effective_working_state") or "").strip() or "stopped",
24+
"effective_working_reason": str(payload.get("effective_working_reason") or "").strip(),
25+
"effective_working_updated_at": payload.get("effective_working_updated_at"),
26+
"effective_active_task_id": payload.get("effective_active_task_id"),
27+
}
28+
with _LOCK:
29+
_BY_GROUP[gid] = normalized
30+
31+
32+
def get_group_runtime(group_id: str) -> Dict[str, Dict[str, Any]]:
33+
gid = str(group_id or "").strip()
34+
if not gid:
35+
return {}
36+
with _LOCK:
37+
current = _BY_GROUP.get(gid) or {}
38+
return {actor_id: dict(payload) for actor_id, payload in current.items()}

src/cccc/daemon/actors/actor_lifecycle_ops.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from ...kernel.group import load_group
1212
from ...kernel.ledger import append_event
1313
from ...kernel.permissions import require_actor_permission
14+
from ...kernel.runtime import inject_runtime_home_env
15+
from ..codex_app_sessions import SUPERVISOR as codex_app_supervisor
1416
from ...runners import headless as headless_runner
1517
from ...runners import pty as pty_runner
1618
from ...util.conv import coerce_bool
@@ -114,7 +116,12 @@ def handle_actor_stop(
114116
actor = update_actor(group, actor_id, {"enabled": False})
115117
runner_kind = str(actor.get("runner") or "pty").strip()
116118
runner_effective = effective_runner_kind(runner_kind)
117-
if runner_effective == "headless":
119+
runtime = str(actor.get("runtime") or "codex").strip() or "codex"
120+
if runtime == "codex" and runner_effective == "headless":
121+
codex_app_supervisor.stop_actor(group_id=group.group_id, actor_id=actor_id)
122+
remove_headless_state(group.group_id, actor_id)
123+
remove_pty_state_if_pid(group.group_id, actor_id, pid=0)
124+
elif runner_effective == "headless":
118125
headless_runner.SUPERVISOR.stop_actor(group_id=group.group_id, actor_id=actor_id)
119126
remove_headless_state(group.group_id, actor_id)
120127
remove_pty_state_if_pid(group.group_id, actor_id, pid=0)
@@ -211,8 +218,12 @@ def handle_actor_restart(
211218
is_admin=is_admin,
212219
)
213220
runner_kind = str(actor.get("runner") or "pty").strip()
214-
runner_effective = effective_runner_kind(runner_kind)
215-
if runner_effective == "headless":
221+
runtime = str(actor.get("runtime") or "codex").strip() or "codex"
222+
if runtime == "codex" and effective_runner_kind(runner_kind) == "headless":
223+
codex_app_supervisor.stop_actor(group_id=group.group_id, actor_id=actor_id)
224+
remove_headless_state(group.group_id, actor_id)
225+
remove_pty_state_if_pid(group.group_id, actor_id, pid=0)
226+
elif effective_runner_kind(runner_kind) == "headless":
216227
headless_runner.SUPERVISOR.stop_actor(group_id=group.group_id, actor_id=actor_id)
217228
remove_headless_state(group.group_id, actor_id)
218229
remove_pty_state_if_pid(group.group_id, actor_id, pid=0)
@@ -308,6 +319,12 @@ def handle_actor_restart(
308319
runner_kind = str(launch_spec["runner"])
309320
runner_effective = str(launch_spec["effective_runner"])
310321
runtime = str(launch_spec["runtime"])
322+
effective_env = inject_runtime_home_env(
323+
launch_spec["merged_env"],
324+
runtime=runtime,
325+
group_id=group.group_id,
326+
actor_id=actor_id,
327+
)
311328
if runner_effective != "headless":
312329
try:
313330
mcp_ready = bool(ensure_mcp_installed(runtime, cwd))
@@ -316,12 +333,19 @@ def handle_actor_restart(
316333
if not mcp_ready:
317334
return _error("actor_restart_failed", f"failed to install MCP for runtime: {runtime}")
318335

319-
if runner_effective == "headless":
336+
if runtime == "codex" and runner_effective == "headless":
337+
codex_app_supervisor.start_actor(
338+
group_id=group.group_id,
339+
actor_id=actor_id,
340+
cwd=cwd,
341+
env=dict(inject_actor_context_env(effective_env, group_id=group.group_id, actor_id=actor_id)),
342+
)
343+
elif runner_effective == "headless":
320344
headless_runner.SUPERVISOR.start_actor(
321345
group_id=group.group_id,
322346
actor_id=actor_id,
323347
cwd=cwd,
324-
env=dict(inject_actor_context_env(launch_spec["merged_env"], group_id=group.group_id, actor_id=actor_id)),
348+
env=dict(inject_actor_context_env(effective_env, group_id=group.group_id, actor_id=actor_id)),
325349
)
326350
try:
327351
write_headless_state(group.group_id, actor_id)
@@ -333,9 +357,8 @@ def handle_actor_restart(
333357
actor_id=actor_id,
334358
cwd=cwd,
335359
command=launch_spec["effective_command"],
336-
env=prepare_pty_env(
337-
inject_actor_context_env(launch_spec["merged_env"], group_id=group.group_id, actor_id=actor_id)
338-
),
360+
env=prepare_pty_env(inject_actor_context_env(effective_env, group_id=group.group_id, actor_id=actor_id)),
361+
runtime=runtime,
339362
max_backlog_bytes=pty_backlog_bytes(),
340363
)
341364
try:
@@ -346,6 +369,13 @@ def handle_actor_restart(
346369
ContextStorage(group).clear_agent_status_if_present(actor_id)
347370
except Exception:
348371
pass
372+
try:
373+
if str(group.doc.get("state") or "").strip() == "stopped":
374+
group.doc["state"] = "active"
375+
group.doc["running"] = True
376+
group.save()
377+
except Exception:
378+
pass
349379

350380
maybe_reset_automation_on_foreman_change(group, before_foreman_id=before_foreman)
351381
event = append_event(
@@ -354,7 +384,11 @@ def handle_actor_restart(
354384
group_id=group.group_id,
355385
scope_key="",
356386
by=by,
357-
data={"actor_id": actor_id, "runner": str(actor.get("runner") or "pty")},
387+
data={
388+
"actor_id": actor_id,
389+
"runner": str(actor.get("runner") or "pty"),
390+
"runner_effective": runner_effective,
391+
},
358392
)
359393

360394
from ...kernel.events import publish_event

src/cccc/daemon/actors/actor_membership_ops.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
from ...contracts.v1 import DaemonError, DaemonResponse
88
from ...kernel.actors import find_actor, list_actors, remove_actor
99
from ...kernel.context import ContextStorage
10+
from ...kernel.events import publish_event
1011
from ...kernel.group import load_group
1112
from ...kernel.ledger import append_event
1213
from ...kernel.permissions import require_actor_permission
1314
from ...runners import headless as headless_runner
1415
from ...runners import pty as pty_runner
1516
from ...util.conv import coerce_bool
17+
from ..codex_app_sessions import SUPERVISOR as codex_app_supervisor
1618
from ..context.context_ops import _schedule_summary_snapshot_rebuild
1719

1820

@@ -47,6 +49,7 @@ def handle_actor_remove(
4749
if isinstance(actor_doc, dict):
4850
avatar_rel_path = str(actor_doc.get("avatar_asset_path") or "").strip()
4951
remove_actor(group, actor_id)
52+
codex_app_supervisor.stop_actor(group_id=group.group_id, actor_id=actor_id)
5053
pty_runner.SUPERVISOR.stop_actor(group_id=group.group_id, actor_id=actor_id)
5154
remove_pty_state_if_pid(group.group_id, actor_id, pid=0)
5255
headless_runner.SUPERVISOR.stop_actor(group_id=group.group_id, actor_id=actor_id)
@@ -84,6 +87,7 @@ def handle_actor_remove(
8487
by=by,
8588
data={"actor_id": actor_id},
8689
)
90+
publish_event("actor.remove", {"group_id": group.group_id, "actor_id": actor_id})
8791
maybe_reset_automation_on_foreman_change(group, before_foreman_id=before_foreman)
8892
return DaemonResponse(ok=True, result={"actor_id": actor_id, "event": event})
8993

src/cccc/daemon/actors/actor_ops.py

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
from typing import Any, Callable, Dict, Optional
66

77
from ...contracts.v1 import DaemonError, DaemonResponse
8+
from ..actor_runtime_cache import get_group_runtime
9+
from ..codex_app_sessions import SUPERVISOR as codex_app_supervisor
810
from ...kernel.actors import find_actor, get_effective_role, list_actors
911
from ...kernel.context import ContextStorage
1012
from ...kernel.group import load_group
1113
from ...kernel.query_projections import get_actor_list_projection
1214
from ...kernel.working_state import DEFAULT_PTY_TERMINAL_SIGNAL_TAIL_BYTES, derive_effective_working_state
13-
from ..context.context_ops import _agent_state_to_dict
14-
from .private_env_ops import mask_private_env_value
1515
from ...runners import headless as headless_runner
1616
from ...runners import pty as pty_runner
17+
from ..context.context_ops import _agent_state_to_dict
18+
from .private_env_ops import mask_private_env_value
1719
from ...util.conv import coerce_bool
1820

1921

@@ -47,6 +49,7 @@ def handle_actor_list(
4749
actors.append(item)
4850
else:
4951
actors = get_actor_list_projection(group)
52+
runtime_snapshot = get_group_runtime(group_id)
5053
storage = ContextStorage(group)
5154
agent_rows = [_agent_state_to_dict(agent) for agent in storage.load_agents().agents]
5255
agent_state_by_id = {
@@ -59,39 +62,61 @@ def handle_actor_list(
5962
if not aid:
6063
continue
6164
runner_kind = str(actor.get("runner") or "pty").strip()
65+
snap = runtime_snapshot.get(aid) if isinstance(runtime_snapshot.get(aid), dict) else {}
66+
if snap:
67+
actor["running"] = bool(snap.get("running"))
68+
actor["idle_seconds"] = snap.get("idle_seconds")
69+
snap_runner_effective = str(snap.get("runner_effective") or runner_kind or "pty")
70+
if snap_runner_effective == "headless" or snap_runner_effective != runner_kind:
71+
actor["runner_effective"] = snap_runner_effective
72+
else:
73+
actor.pop("runner_effective", None)
74+
for key in (
75+
"effective_working_state",
76+
"effective_working_reason",
77+
"effective_working_updated_at",
78+
"effective_active_task_id",
79+
):
80+
if key in snap:
81+
actor[key] = snap.get(key)
82+
continue
6283
effective_runner = effective_runner_kind(runner_kind)
63-
idle_seconds = None
84+
runtime = str(actor.get("runtime") or "").strip()
6485
headless_state = None
65-
if effective_runner == "headless":
66-
actor["running"] = headless_runner.SUPERVISOR.actor_running(group_id, aid)
86+
running = False
87+
idle_seconds = None
88+
pty_terminal_text = ""
89+
if runtime.lower() == "codex" and effective_runner == "headless":
90+
state = codex_app_supervisor.get_state(group_id=group_id, actor_id=aid)
91+
headless_state = state.model_dump() if hasattr(state, "model_dump") else (dict(state) if isinstance(state, dict) else None)
92+
running = bool(state is not None and codex_app_supervisor.actor_running(group_id, aid))
93+
elif effective_runner == "headless":
6794
state = headless_runner.SUPERVISOR.get_state(group_id=group_id, actor_id=aid)
68-
headless_state = state.model_dump() if state is not None else None
69-
actor["idle_seconds"] = None
95+
headless_state = state.model_dump() if hasattr(state, "model_dump") else (dict(state) if isinstance(state, dict) else None)
96+
running = bool(state is not None and headless_runner.SUPERVISOR.actor_running(group_id, aid))
7097
else:
71-
actor["running"] = pty_runner.SUPERVISOR.actor_running(group_id, aid)
72-
idle_seconds = (
73-
pty_runner.SUPERVISOR.idle_seconds(group_id=group_id, actor_id=aid)
74-
if actor["running"]
75-
else None
76-
)
77-
actor["idle_seconds"] = idle_seconds
78-
pty_terminal_text = ""
79-
if effective_runner == "pty" and actor["running"]:
80-
try:
81-
pty_terminal_text = pty_runner.SUPERVISOR.tail_output(
82-
group_id=group_id,
83-
actor_id=aid,
84-
max_bytes=DEFAULT_PTY_TERMINAL_SIGNAL_TAIL_BYTES,
85-
).decode("utf-8", errors="replace")
86-
except Exception:
87-
pty_terminal_text = ""
88-
if effective_runner != runner_kind:
98+
running = bool(pty_runner.SUPERVISOR.actor_running(group_id, aid))
99+
idle_seconds = pty_runner.SUPERVISOR.idle_seconds(group_id=group_id, actor_id=aid) if running else None
100+
if running:
101+
try:
102+
pty_terminal_text = pty_runner.SUPERVISOR.tail_output(
103+
group_id=group_id,
104+
actor_id=aid,
105+
max_bytes=DEFAULT_PTY_TERMINAL_SIGNAL_TAIL_BYTES,
106+
).decode("utf-8", errors="replace")
107+
except Exception:
108+
pty_terminal_text = ""
109+
actor["running"] = running
110+
actor["idle_seconds"] = idle_seconds
111+
if effective_runner == "headless" or effective_runner != runner_kind:
89112
actor["runner_effective"] = effective_runner
113+
else:
114+
actor.pop("runner_effective", None)
90115
actor.update(
91116
derive_effective_working_state(
92-
running=bool(actor.get("running")),
117+
running=running,
93118
effective_runner=effective_runner,
94-
runtime=str(actor.get("runtime") or ""),
119+
runtime=runtime,
95120
idle_seconds=idle_seconds,
96121
pty_terminal_text=pty_terminal_text,
97122
agent_state=agent_state_by_id.get(aid),

0 commit comments

Comments
 (0)