66"""
77from __future__ import annotations
88
9+ import logging
910import os
1011from dataclasses import dataclass , field
1112from 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+
209223def _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 ):
0 commit comments