Coverage for core / safe_hartos_attr.py: 71.1%
45 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""Singleton accessor for the canonical hart_intelligence modules.
3Worker threads must NOT do ``from hart_intelligence import X`` directly.
4The hart_intelligence module pulls in transformers / langchain / autogen
5on first import — a chain that holds Python's per-module import lock for
6minutes on cold disk. When a worker thread races the canonical loader
7(``hartos_backend_adapter._attempt_hartos_init``) on that same lock, the
8worker stalls forever — the symptom shape is `Tier-1
9ACTIVE` never logs, chat falls through to fallback path, daemon's
10``is_user_recently_active()`` gate is unreachable, MMLU runaway can't be
11stopped, etc).
13The singleton is ``sys.modules['hart_intelligence']`` — Python guarantees
14a single dict entry per module. ONE writer (the canonical loader) does
15the slow import. Every other reader uses this accessor. No new lock,
16no new mechanism — we're just naming the singleton that already exists.
18Observability
19=============
20Every resolution is logged so silent no-ops are never invisible:
22* HIT — symbol resolved, returned to caller (DEBUG, with module name).
23* MISS — HARTOS not yet loaded; first miss per symbol → INFO (loud once),
24 subsequent identical misses → DEBUG (quiet). This way the
25 logs show the *transition* without flooding when many workers
26 hit the same not-yet-loaded symbol every tick.
27* MISS_ATTR — modules are loaded but the requested attribute isn't
28 bound (mid-init or symbol typo). Always INFO — distinct
29 from "loader hasn't started" because the diagnosis is
30 different.
32Usage
33=====
34::
36 from core.safe_hartos_attr import safe_hartos_attr
38 publish_async = safe_hartos_attr('publish_async')
39 if publish_async is not None:
40 publish_async(topic, msg)
41 # else: HARTOS not yet loaded — caller no-ops, same as the
42 # `try: from hart_intelligence import …; except ImportError: pass`
43 # pattern this replaces, but without triggering the import lock.
45Enforcement
46===========
47``[tool.ruff.lint.flake8-tidy-imports.banned-api]`` in HARTOS
48``pyproject.toml`` bans ``from hart_intelligence import …`` and
49``from hart_intelligence_entry import …`` everywhere except the
50canonical loader (which carries ``# noqa: TID251`` with a comment).
51"""
52from __future__ import annotations
54import logging
55import sys
56import threading
57from typing import Any
59logger = logging.getLogger('core.safe_hartos_attr')
61_FACADE = 'hart_intelligence'
62_ENTRY = 'hart_intelligence_entry'
64# Tracks (symbol_name, was_loaded_at_check_time) tuples we've already
65# logged at INFO. Subsequent identical misses fall to DEBUG so a worker
66# polling every tick doesn't flood the log. Reset implicitly on process
67# restart; cleared explicitly by ``reset_log_dedup()`` for tests.
68_logged_states: set[tuple[str, str]] = set()
69_dedup_lock = threading.Lock()
72def _log_outcome(name: str, outcome: str, *, mod_name: str | None = None) -> None:
73 """Log a resolution outcome, deduplicating identical (symbol, outcome) pairs.
75 First occurrence at INFO, subsequent at DEBUG — gives a loud signal
76 on state transitions (loader finishes, symbol becomes resolvable)
77 while keeping steady-state ticks quiet.
78 """
79 key = (name, outcome)
80 with _dedup_lock:
81 first = key not in _logged_states
82 if first:
83 _logged_states.add(key)
84 if outcome == 'HIT':
85 # HITs are normal once HARTOS is up — DEBUG always, single
86 # transition log on first hit per symbol so we can see "loader
87 # came online" in the timeline.
88 if first:
89 logger.info(
90 "safe_hartos_attr HIT name=%s mod=%s (first resolution — "
91 "HARTOS now reachable for this symbol)",
92 name, mod_name,
93 )
94 else:
95 logger.debug("safe_hartos_attr HIT name=%s mod=%s", name, mod_name)
96 elif outcome == 'MISS':
97 if first:
98 logger.info(
99 "safe_hartos_attr MISS name=%s — HARTOS not yet loaded "
100 "(canonical loader hartos_backend_adapter._attempt_hartos_init "
101 "hasn't published sys.modules['hart_intelligence']). "
102 "Caller will no-op gracefully; subsequent identical misses "
103 "logged at DEBUG.",
104 name,
105 )
106 else:
107 logger.debug("safe_hartos_attr MISS name=%s", name)
108 elif outcome == 'MISS_ATTR':
109 # Different bug shape from MISS: modules are loaded but symbol
110 # not bound. Always INFO — caller likely typo'd or symbol moved.
111 logger.info(
112 "safe_hartos_attr MISS_ATTR name=%s mod=%s — module loaded "
113 "but attribute not bound (typo? renamed? mid-init?)",
114 name, mod_name,
115 )
118def safe_hartos_attr(name: str, default: Any = None) -> Any:
119 """Read a symbol from the loaded hart_intelligence singleton.
121 Resolution order:
122 1. ``sys.modules['hart_intelligence']`` (the canonical facade)
123 2. ``sys.modules['hart_intelligence_entry']`` (the implementation)
124 3. ``default`` if neither is loaded yet
126 The facade re-exports everything from the entry module via
127 ``from hart_intelligence_entry import *``, so once the loader has
128 finished, both names point to the same symbol set — but during early
129 boot only the entry module may be partially loaded. Checking both
130 keeps callers working at every loader stage without a single false
131 ImportError on the cold path.
133 NEVER triggers an import. If neither module is in ``sys.modules``,
134 returns ``default``. Caller is expected to handle ``None``/``default``
135 by no-op'ing — exactly the shape ``idle_detection.get_idle_opted_in_agents``
136 and ``world_model_bridge._init_in_process`` already use.
138 Every call is logged (deduplicated) so silent fall-throughs are
139 never invisible — see module docstring for log levels.
141 Args:
142 name: attribute name to look up (e.g. ``'publish_async'``).
143 default: value to return if HARTOS isn't loaded or the attribute
144 doesn't exist. Defaults to ``None``.
146 Returns:
147 The attribute value, or ``default``.
148 """
149 facade = sys.modules.get(_FACADE)
150 entry = sys.modules.get(_ENTRY)
151 mod = facade or entry
152 if mod is None:
153 _log_outcome(name, 'MISS')
154 return default
155 mod_name = _FACADE if facade is not None else _ENTRY
156 val = getattr(mod, name, None)
157 if val is None:
158 _log_outcome(name, 'MISS_ATTR', mod_name=mod_name)
159 return default
160 _log_outcome(name, 'HIT', mod_name=mod_name)
161 return val
164def hartos_loaded() -> bool:
165 """True if the canonical loader has finished and the facade is ready.
167 Cheaper than ``safe_hartos_attr`` for "is it ready?" checks where the
168 caller doesn't need a specific symbol — no ``getattr`` traversal,
169 just a dict lookup. Use this for short-circuit gates::
171 if not hartos_loaded():
172 return # nothing for us to do yet
174 A ``True`` result means at least one of the two module names is in
175 ``sys.modules``; it does NOT promise every symbol the loader will
176 eventually publish is bound yet (mid-init reads can still see
177 ``None`` from ``safe_hartos_attr``). Use ``safe_hartos_attr`` with
178 a defensive ``if x is not None`` for the actual call.
179 """
180 return _FACADE in sys.modules or _ENTRY in sys.modules
183def reset_log_dedup() -> None:
184 """Clear the (symbol, outcome) dedup memory.
186 Tests use this to assert on first-time INFO emission without process
187 restart noise. Production code should not call this — repeated INFO
188 logs of the same state transition are pure noise on the chat hot path.
189 """
190 with _dedup_lock:
191 _logged_states.clear()