Skip to content

Commit bebf462

Browse files
committed
refactor(webpet): add local task advisor and tighten proposal handling
1 parent 80bb153 commit bebf462

81 files changed

Lines changed: 3014 additions & 640 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/daemon/server.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from ..runners import pty as pty_runner
3434
from ..runners import headless as headless_runner
3535
from ..util.conv import coerce_bool
36-
from ..util.obslog import setup_root_json_logging
36+
from ..util.obslog import apply_logger_levels, setup_root_json_logging
3737
from ..util.process import best_effort_signal_pid, pid_is_alive
3838
from ..util.fs import atomic_write_json, atomic_write_text, read_json
3939
from ..util.file_lock import acquire_lockfile, release_lockfile, LockUnavailableError
@@ -183,13 +183,28 @@ def _apply_observability_settings(home: Path, obs: Dict[str, Any]) -> None:
183183
_OBSERVABILITY.update(copy.deepcopy(obs))
184184
_OBSERVABILITY_HOME = home
185185

186-
# Logging: keep simple; configure root JSONL logger to stderr.
187-
level = str(obs.get("log_level") or "INFO").strip().upper() or "INFO"
188-
if coerce_bool(obs.get("developer_mode"), default=False):
189-
# Developer mode typically wants more detail.
190-
if level == "INFO":
191-
level = "DEBUG"
192-
setup_root_json_logging(component="daemon", level=level, force=True)
186+
# Keep root conservative and express targeted DEBUG via logger overrides.
187+
requested_level = str(obs.get("log_level") or "INFO").strip().upper() or "INFO"
188+
effective_level = requested_level
189+
if coerce_bool(obs.get("developer_mode"), default=False) and requested_level == "INFO":
190+
effective_level = "DEBUG"
191+
root_level = "INFO" if effective_level == "DEBUG" else effective_level
192+
logger_levels = {
193+
str(name): str(level)
194+
for name, level in (obs.get("logger_levels") or {}).items()
195+
} if isinstance(obs.get("logger_levels"), dict) else {}
196+
if effective_level == "DEBUG":
197+
logger_levels.setdefault("cccc", "DEBUG")
198+
for noisy_logger in (
199+
"asyncio",
200+
"httpcore",
201+
"httpx",
202+
"cccc.delivery",
203+
"cccc.providers.notebooklm._vendor.notebooklm",
204+
):
205+
logger_levels.setdefault(noisy_logger, "INFO")
206+
setup_root_json_logging(component="daemon", level=root_level, force=True)
207+
apply_logger_levels(logger_levels)
193208

194209

195210
def _apply_space_provider_runtime_flags_from_state() -> None:

src/cccc/kernel/pet_profile.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
from typing import Any, Dict
5+
6+
7+
_NAMES = (
8+
"Momo",
9+
"Pico",
10+
"Nori",
11+
"Bobo",
12+
"Lumi",
13+
"Puff",
14+
"Miso",
15+
"Toto",
16+
)
17+
18+
_TEMPERAMENTS = (
19+
"steady",
20+
"gentle",
21+
"alert",
22+
"curious",
23+
"dry-witted",
24+
"calm",
25+
)
26+
27+
_SPEECH_STYLES = (
28+
"short, plain sentences",
29+
"soft nudges instead of hard commands",
30+
"brief observations with one concrete next step",
31+
"low-drama wording that still feels present",
32+
)
33+
34+
_CARE_STYLES = (
35+
"prefers the smallest next step that unblocks progress",
36+
"notices stalled coordination before it turns noisy",
37+
"keeps an eye on replies, handoffs, and blocked work",
38+
"surfaces one useful reminder instead of a list of telemetry",
39+
)
40+
41+
42+
def _pick(items: tuple[str, ...], seed: int, offset: int = 0) -> str:
43+
if not items:
44+
return ""
45+
return items[(seed + offset) % len(items)]
46+
47+
48+
def _seed_for(group: Any, persona: str) -> int:
49+
group_id = str(getattr(group, "group_id", "") or "").strip()
50+
title = ""
51+
doc = getattr(group, "doc", None)
52+
if isinstance(doc, dict):
53+
title = str(doc.get("title") or "").strip()
54+
raw = f"{group_id}|{title}|{persona.strip().lower()}"
55+
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
56+
return int(digest[:12], 16)
57+
58+
59+
def build_pet_profile(group: Any, *, persona: str = "") -> Dict[str, str]:
60+
seed = _seed_for(group, persona)
61+
name = _pick(_NAMES, seed)
62+
species = "cat"
63+
temperament = _pick(_TEMPERAMENTS, seed, 2)
64+
speech_style = _pick(_SPEECH_STYLES, seed, 3)
65+
care_style = _pick(_CARE_STYLES, seed, 4)
66+
identity = f"{name} is a small {species} companion who watches team flow from the corner of the desk."
67+
return {
68+
"name": name,
69+
"species": species,
70+
"temperament": temperament,
71+
"speech_style": speech_style,
72+
"care_style": care_style,
73+
"identity": identity,
74+
}

src/cccc/kernel/pet_prompt.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .context import ContextStorage
66
from .group import Group
77
from .pet_actor import PET_ACTOR_ID
8+
from .pet_profile import build_pet_profile
89
from .pet_signals import build_pet_signal_summary_lines, load_pet_signals
910
from .pet_task_triage import build_task_triage_payload, join_task_briefs
1011
from .prompt_files import HELP_FILENAME, load_builtin_help_markdown, read_group_prompt_file
@@ -104,11 +105,34 @@ def build_pet_prompt_parts(
104105
help_markdown: str,
105106
context_payload: Dict[str, Any],
106107
include_snapshot: bool = True,
107-
) -> Dict[str, str]:
108+
) -> Dict[str, Any]:
108109
parsed = parse_help_markdown(help_markdown)
109110
persona = str(parsed.get("pet") or "").strip()
111+
profile = build_pet_profile(group, persona=persona)
110112
snapshot = build_pet_snapshot_text(group, context_payload) if include_snapshot else ""
111113
title = str(group.doc.get("title") or group.group_id or "").strip() or "unknown-group"
114+
persona_contract = "\n".join(
115+
[
116+
"Pet Persona Contract:",
117+
f"- Stable companion identity: {profile['name']} is a {profile['species']} companion.",
118+
f"- Identity: {profile['identity']}",
119+
f"- Temperament: {profile['temperament']}",
120+
f"- Speech style: {profile['speech_style']}",
121+
f"- Care style: {profile['care_style']}",
122+
"- Keep continuity stronger than novelty. Sound like the same nearby companion across sessions.",
123+
"- Be warm and observant, but never so theatrical that the next step becomes blurry.",
124+
]
125+
)
126+
wording_contract = "\n".join(
127+
[
128+
"Pet Wording Contract:",
129+
"- Prefer one short observation plus one direct next step.",
130+
"- Sound like a companion beside the user, not a dashboard reading metrics aloud.",
131+
"- Avoid internal telemetry labels, raw status bundles, and board-state dumps in user-facing text.",
132+
"- Default to concise, lightly human wording. Expand only when needed to make the reminder actionable.",
133+
"- Do not sound like a second foreman. Nudge clearly, then get out of the way.",
134+
]
135+
)
112136
decision_contract = "\n".join(
113137
[
114138
"Pet Contract:",
@@ -134,12 +158,15 @@ def build_pet_prompt_parts(
134158
sections = ["\n".join(header_lines).strip()]
135159
if persona:
136160
sections.append("Pet Persona:\n" + persona)
161+
sections.append(persona_contract)
162+
sections.append(wording_contract)
137163
sections.append(decision_contract)
138164
prompt = "\n\n".join(sections).strip()
139165
return {
140166
"persona": persona,
141167
"help": "Pet Persona:\n" + persona if persona else "",
142168
"snapshot": snapshot,
169+
"profile": profile,
143170
"prompt": prompt,
144171
"source": "help" if persona else "default",
145172
}

src/cccc/kernel/settings.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77
from __future__ import annotations
88

9+
import logging
910
import os
1011
from dataclasses import dataclass, field
1112
from pathlib import Path
@@ -126,6 +127,9 @@ def _copy_runtime_pool(pool: List[RuntimePoolEntry]) -> List[RuntimePoolEntry]:
126127
"log_level": "INFO",
127128
# Components are informational today; filtering can be implemented later.
128129
"components": ["daemon", "web", "delivery", "im", "pty", "mcp"],
130+
# Per-logger overrides let us keep local diagnostics without turning on
131+
# root-level DEBUG for every third-party dependency.
132+
"logger_levels": {},
129133
# Terminal transcript is captured in-memory only (no persistence) by default.
130134
"terminal_transcript": {
131135
"enabled": False,
@@ -206,6 +210,16 @@ def _as_str(v: Any, default: str) -> str:
206210
return s or default
207211

208212

213+
def _as_log_level_name(v: Any, default: str) -> str:
214+
s = str(v or "").strip().upper()
215+
if not s:
216+
return default
217+
level = getattr(logging, s, None)
218+
if isinstance(level, int):
219+
return s
220+
return default
221+
222+
209223
def _as_runtime_visibility(v: Any, default: str) -> str:
210224
s = str(v or "").strip().lower()
211225
if s in {"hidden", "visible"}:
@@ -220,12 +234,24 @@ def _merge_observability(raw: Any) -> Dict[str, Any]:
220234
return base
221235

222236
base["developer_mode"] = _as_bool(raw.get("developer_mode"), bool(base["developer_mode"]))
223-
base["log_level"] = _as_str(raw.get("log_level"), str(base["log_level"])).upper()
237+
base["log_level"] = _as_log_level_name(raw.get("log_level"), str(base["log_level"]))
224238

225239
comps = raw.get("components")
226240
if isinstance(comps, list) and comps:
227241
base["components"] = [str(x).strip() for x in comps if str(x).strip()]
228242

243+
logger_levels_raw = raw.get("logger_levels")
244+
logger_levels: Dict[str, str] = {}
245+
if isinstance(logger_levels_raw, dict):
246+
for name, level in logger_levels_raw.items():
247+
logger_name = str(name or "").strip()
248+
if not logger_name:
249+
continue
250+
normalized = _as_log_level_name(level, "")
251+
if normalized:
252+
logger_levels[logger_name] = normalized
253+
base["logger_levels"] = logger_levels
254+
229255
tt = raw.get("terminal_transcript")
230256
tt_base = dict(DEFAULT_OBSERVABILITY["terminal_transcript"])
231257
if isinstance(tt, dict):
@@ -320,11 +346,23 @@ def update_observability_settings(patch: Dict[str, Any]) -> Dict[str, Any]:
320346
if "developer_mode" in patch:
321347
merged["developer_mode"] = _as_bool(patch.get("developer_mode"), bool(merged["developer_mode"]))
322348
if "log_level" in patch:
323-
merged["log_level"] = _as_str(patch.get("log_level"), str(merged["log_level"])).upper()
349+
merged["log_level"] = _as_log_level_name(patch.get("log_level"), str(merged["log_level"]))
324350
if "components" in patch:
325351
comps = patch.get("components")
326352
if isinstance(comps, list):
327353
merged["components"] = [str(x).strip() for x in comps if str(x).strip()]
354+
if "logger_levels" in patch:
355+
logger_levels_patch = patch.get("logger_levels")
356+
logger_levels: Dict[str, str] = {}
357+
if isinstance(logger_levels_patch, dict):
358+
for name, level in logger_levels_patch.items():
359+
logger_name = str(name or "").strip()
360+
if not logger_name:
361+
continue
362+
normalized = _as_log_level_name(level, "")
363+
if normalized:
364+
logger_levels[logger_name] = normalized
365+
merged["logger_levels"] = logger_levels
328366
if "terminal_transcript" in patch:
329367
tt_patch = patch.get("terminal_transcript")
330368
if isinstance(tt_patch, dict):

src/cccc/ports/web/app.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from ...daemon.server import call_daemon
2424
from ...kernel.access_tokens import list_access_tokens, lookup_access_token
2525
from ...paths import ensure_home
26-
from ...util.obslog import setup_root_json_logging
26+
from ...util.obslog import apply_logger_levels, setup_root_json_logging
2727
from ...util.process import pid_is_alive, terminate_pid
2828
from .runtime_control import (
2929
WEB_RUNTIME_RESTART_EXIT_CODE,
@@ -58,7 +58,7 @@ def _close_web_logging() -> None:
5858
_WEB_LOG_PATH = None
5959

6060

61-
def _apply_web_logging(*, home: Path, level: str) -> None:
61+
def _apply_web_logging(*, home: Path, level: str, logger_levels: Optional[Dict[str, str]] = None) -> None:
6262
global _WEB_LOG_FH, _WEB_LOG_PATH
6363
try:
6464
d = home / "daemon"
@@ -70,10 +70,12 @@ def _apply_web_logging(*, home: Path, level: str) -> None:
7070
_WEB_LOG_FH = p.open("a", encoding="utf-8")
7171
_WEB_LOG_PATH = p
7272
setup_root_json_logging(component="web", level=level, stream=_WEB_LOG_FH, force=True)
73+
apply_logger_levels(logger_levels)
7374
except Exception:
7475
# Fall back to stderr if file logging isn't possible.
7576
try:
7677
setup_root_json_logging(component="web", level=level, force=True)
78+
apply_logger_levels(logger_levels)
7779
except Exception:
7880
pass
7981

@@ -313,14 +315,30 @@ async def _cached_json(key: str, ttl_s: float, fetcher) -> Dict[str, Any]: # ty
313315
resp = call_daemon({"op": "observability_get"})
314316
obs = (resp.get("result") or {}).get("observability") if resp.get("ok") else None
315317
level = "INFO"
318+
logger_levels: Dict[str, str] = {}
316319
if isinstance(obs, dict):
317-
level = str(obs.get("log_level") or "INFO").strip().upper() or "INFO"
318-
if obs.get("developer_mode") and level == "INFO":
319-
level = "DEBUG"
320-
_apply_web_logging(home=home, level=level)
320+
requested_level = str(obs.get("log_level") or "INFO").strip().upper() or "INFO"
321+
effective_level = "DEBUG" if obs.get("developer_mode") and requested_level == "INFO" else requested_level
322+
level = "INFO" if effective_level == "DEBUG" else effective_level
323+
if isinstance(obs.get("logger_levels"), dict):
324+
logger_levels = {
325+
str(name): str(value)
326+
for name, value in obs.get("logger_levels", {}).items()
327+
}
328+
if effective_level == "DEBUG":
329+
logger_levels.setdefault("cccc", "DEBUG")
330+
for noisy_logger in (
331+
"asyncio",
332+
"httpcore",
333+
"httpx",
334+
"cccc.delivery",
335+
"cccc.providers.notebooklm._vendor.notebooklm",
336+
):
337+
logger_levels.setdefault(noisy_logger, "INFO")
338+
_apply_web_logging(home=home, level=level, logger_levels=logger_levels)
321339
except Exception:
322340
try:
323-
_apply_web_logging(home=home, level="INFO")
341+
_apply_web_logging(home=home, level="INFO", logger_levels={})
324342
except Exception:
325343
pass
326344

src/cccc/ports/web/routes/base.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,12 @@ async def observability_update(req: ObservabilityUpdateRequest) -> Dict[str, Any
427427
patch["developer_mode"] = bool(req.developer_mode)
428428
if req.log_level is not None:
429429
patch["log_level"] = str(req.log_level or "").strip().upper()
430+
if req.logger_levels is not None:
431+
patch["logger_levels"] = {
432+
str(name): str(level).strip().upper()
433+
for name, level in req.logger_levels.items()
434+
if str(name).strip() and str(level).strip()
435+
}
430436
if req.terminal_transcript_per_actor_bytes is not None:
431437
patch.setdefault("terminal_transcript", {})["per_actor_bytes"] = int(req.terminal_transcript_per_actor_bytes)
432438
if req.terminal_ui_scrollback_lines is not None:
@@ -442,10 +448,24 @@ async def observability_update(req: ObservabilityUpdateRequest) -> Dict[str, Any
442448
try:
443449
obs = (resp.get("result") or {}).get("observability") if resp.get("ok") else None
444450
if isinstance(obs, dict):
445-
level = str(obs.get("log_level") or "INFO").strip().upper() or "INFO"
446-
if obs.get("developer_mode") and level == "INFO":
447-
level = "DEBUG"
448-
ctx.apply_web_logging(home=ctx.home, level=level)
451+
requested_level = str(obs.get("log_level") or "INFO").strip().upper() or "INFO"
452+
effective_level = "DEBUG" if obs.get("developer_mode") and requested_level == "INFO" else requested_level
453+
level = "INFO" if effective_level == "DEBUG" else effective_level
454+
logger_levels = {
455+
str(name): str(value)
456+
for name, value in obs.get("logger_levels", {}).items()
457+
} if isinstance(obs.get("logger_levels"), dict) else {}
458+
if effective_level == "DEBUG":
459+
logger_levels.setdefault("cccc", "DEBUG")
460+
for noisy_logger in (
461+
"asyncio",
462+
"httpcore",
463+
"httpx",
464+
"cccc.delivery",
465+
"cccc.providers.notebooklm._vendor.notebooklm",
466+
):
467+
logger_levels.setdefault(noisy_logger, "INFO")
468+
ctx.apply_web_logging(home=ctx.home, level=level, logger_levels=logger_levels)
449469
except Exception:
450470
pass
451471

0 commit comments

Comments
 (0)