personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(local): real display-powersave drain-gate condition

Make the inert display_powersave drain-gate seam real. While the host's
primary display is asleep/in powersave the deferred-processing drain gate
opens (debounced ~120s, wake closes immediately); when display state can't
be reliably detected the condition reports undetectable and the owner falls
back to the time window — it never claims asleep when it can't tell.

New solstone/think/display_powersave.py: a read-only, exception-total
detector (Linux DRM sysfs dpms aggregation; macOS CoreGraphics
CGDisplayIsAsleep via ctypes + a pmset-log fallback when there is no
window-server session) plus an in-memory monotonic-clock debouncer.
evaluate_display_powersave/evaluate_drain_gate become pure functions of a
passed-in display reading; only the supervisor gate-tick polls the detector
(and only when the condition is enabled), while the health surface reads the
last-known snapshot, so health reads do no OS I/O and never advance the
debounce. A sticky display_powersave_detectable field is exposed on the
health surface (data only; never feeds gate open/closed).

Supersedes note: the parent spec names the Linux mechanism "X11 DPMS" and
calls Wayland the undetectable case. This beneficially supersedes that — the
DE-independent sysfs DRM signal works under both X11 and Wayland on atomic
drivers (amdgpu/i915/nouveau), narrowing the undetectable population from
"all of Wayland" to "NVIDIA-proprietary / headless". Recorded so a future
reader does not "fix" it back to X11-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

+913 -20
+233
solstone/think/display_powersave.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Display powersave detection and debounced drain-gate monitoring.""" 5 + 6 + from __future__ import annotations 7 + 8 + import ctypes 9 + import logging 10 + import subprocess 11 + import sys 12 + from dataclasses import dataclass 13 + from pathlib import Path 14 + from typing import Callable 15 + 16 + from solstone.think.processing import ( 17 + DISPLAY_POWERSAVE_UNAVAILABLE, 18 + DisplayPowersaveReading, 19 + ) 20 + 21 + logger = logging.getLogger(__name__) 22 + 23 + DISPLAY_ASLEEP = "asleep" 24 + DISPLAY_AWAKE = "awake" 25 + DISPLAY_UNDETECTABLE = "undetectable" 26 + 27 + REASON_HEADLESS = "headless" 28 + REASON_UNREADABLE = "unreadable" 29 + REASON_UNSUPPORTED_PLATFORM = "unsupported_platform" 30 + REASON_NO_WINDOW_SERVER = "no_window_server" 31 + 32 + DISPLAY_DEBOUNCE_S = 120.0 33 + DRM_SYSFS_ROOT = Path("/sys/class/drm") 34 + 35 + _OFF_DPMS = frozenset({"Off", "Standby", "Suspend"}) 36 + _MACOS_DISPLAY_OFF_MARKER = "Display is turned off" 37 + _MACOS_DISPLAY_ON_MARKER = "Display is turned on" 38 + _PMSET_TIMEOUT_S = 5.0 39 + _CORE_GRAPHICS_PATH = "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics" 40 + 41 + 42 + @dataclass(frozen=True) 43 + class DisplayReading: 44 + state: str 45 + reason: str | None 46 + 47 + 48 + def _asleep() -> DisplayReading: 49 + return DisplayReading(DISPLAY_ASLEEP, None) 50 + 51 + 52 + def _awake() -> DisplayReading: 53 + return DisplayReading(DISPLAY_AWAKE, None) 54 + 55 + 56 + def _undetectable(reason: str) -> DisplayReading: 57 + return DisplayReading(DISPLAY_UNDETECTABLE, reason) 58 + 59 + 60 + def read_display_power( 61 + *, 62 + platform: str = sys.platform, 63 + sysfs_root: Path = DRM_SYSFS_ROOT, 64 + ) -> DisplayReading: 65 + """Return the current display power state, or an undetectable reason.""" 66 + try: 67 + if platform.startswith("linux"): 68 + return _read_linux(sysfs_root) 69 + if platform == "darwin": 70 + return _read_macos() 71 + return _undetectable(REASON_UNSUPPORTED_PLATFORM) 72 + except Exception: 73 + logger.debug("display powersave detection failed", exc_info=True) 74 + return _undetectable(REASON_UNREADABLE) 75 + 76 + 77 + def _read_linux(sysfs_root: Path) -> DisplayReading: 78 + try: 79 + connectors = list(sysfs_root.glob("card*-*")) 80 + except OSError: 81 + return _undetectable(REASON_UNREADABLE) 82 + if not connectors: 83 + return _undetectable(REASON_UNREADABLE) 84 + 85 + considered = 0 86 + any_unreadable = False 87 + for connector in connectors: 88 + status = _read_attr(connector, "status") 89 + enabled = _read_attr(connector, "enabled") 90 + if status != "connected" or enabled != "enabled": 91 + continue 92 + dpms = _read_attr(connector, "dpms") 93 + considered += 1 94 + if dpms == "On": 95 + return _awake() 96 + if dpms not in _OFF_DPMS: 97 + any_unreadable = True 98 + 99 + if considered == 0: 100 + return _undetectable(REASON_HEADLESS) 101 + if any_unreadable: 102 + return _undetectable(REASON_UNREADABLE) 103 + return _asleep() 104 + 105 + 106 + def _read_attr(path: Path, name: str) -> str | None: 107 + try: 108 + return (path / name).read_text(encoding="utf-8").strip() 109 + except OSError: 110 + return None 111 + 112 + 113 + def _core_graphics(): 114 + return ctypes.CDLL(_CORE_GRAPHICS_PATH) 115 + 116 + 117 + def _macos_main_display_id() -> int: 118 + core_graphics = _core_graphics() 119 + core_graphics.CGMainDisplayID.restype = ctypes.c_uint32 120 + return int(core_graphics.CGMainDisplayID()) 121 + 122 + 123 + def _macos_display_is_asleep(display_id: int) -> bool: 124 + core_graphics = _core_graphics() 125 + core_graphics.CGDisplayIsAsleep.argtypes = [ctypes.c_uint32] 126 + core_graphics.CGDisplayIsAsleep.restype = ctypes.c_bool 127 + return bool(core_graphics.CGDisplayIsAsleep(display_id)) 128 + 129 + 130 + def _macos_pmset_log() -> str: 131 + try: 132 + completed = subprocess.run( 133 + ["pmset", "-g", "log"], 134 + capture_output=True, 135 + text=True, 136 + timeout=_PMSET_TIMEOUT_S, 137 + check=False, 138 + ) 139 + except (subprocess.TimeoutExpired, OSError): 140 + return "" 141 + return completed.stdout 142 + 143 + 144 + def _read_macos() -> DisplayReading: 145 + main = _macos_main_display_id() 146 + if main == 0: 147 + return _parse_macos_log(_macos_pmset_log()) 148 + if _macos_display_is_asleep(main): 149 + return _asleep() 150 + return _awake() 151 + 152 + 153 + def _parse_macos_log(text: str) -> DisplayReading: 154 + last: str | None = None 155 + for line in text.splitlines(): 156 + if _MACOS_DISPLAY_OFF_MARKER in line: 157 + last = DISPLAY_ASLEEP 158 + elif _MACOS_DISPLAY_ON_MARKER in line: 159 + last = DISPLAY_AWAKE 160 + if last == DISPLAY_ASLEEP: 161 + return _asleep() 162 + if last == DISPLAY_AWAKE: 163 + return _awake() 164 + return _undetectable(REASON_NO_WINDOW_SERVER) 165 + 166 + 167 + class DisplayPowersaveMonitor: 168 + def __init__(self, *, debounce_s: float = DISPLAY_DEBOUNCE_S): 169 + self._debounce_s = debounce_s 170 + self._snapshot = DISPLAY_POWERSAVE_UNAVAILABLE 171 + self._asleep_since: float | None = None 172 + self._ever_detected = False 173 + 174 + def poll( 175 + self, 176 + *, 177 + now: float, 178 + read: Callable[[], DisplayReading] = read_display_power, 179 + ) -> DisplayPowersaveReading: 180 + reading = read() 181 + if reading.state == DISPLAY_ASLEEP: 182 + if self._asleep_since is None: 183 + self._asleep_since = now 184 + snapshot = DisplayPowersaveReading( 185 + available=True, 186 + asleep=True, 187 + debounced=(now - self._asleep_since) >= self._debounce_s, 188 + ) 189 + elif reading.state == DISPLAY_AWAKE: 190 + self._asleep_since = None 191 + snapshot = DisplayPowersaveReading( 192 + available=True, 193 + asleep=False, 194 + debounced=False, 195 + ) 196 + else: 197 + self._asleep_since = None 198 + snapshot = DISPLAY_POWERSAVE_UNAVAILABLE 199 + 200 + if snapshot.available: 201 + self._ever_detected = True 202 + self._snapshot = snapshot 203 + return snapshot 204 + 205 + def last(self) -> DisplayPowersaveReading: 206 + return self._snapshot 207 + 208 + def capability_known(self) -> bool: 209 + return self._ever_detected 210 + 211 + def reset(self) -> None: 212 + self._snapshot = DISPLAY_POWERSAVE_UNAVAILABLE 213 + self._asleep_since = None 214 + self._ever_detected = False 215 + 216 + 217 + _MONITOR = DisplayPowersaveMonitor() 218 + 219 + 220 + def poll_display_powersave(now: float) -> DisplayPowersaveReading: 221 + return _MONITOR.poll(now=now) 222 + 223 + 224 + def last_display_powersave() -> DisplayPowersaveReading: 225 + return _MONITOR.last() 226 + 227 + 228 + def display_powersave_detectable() -> bool: 229 + return _MONITOR.capability_known() 230 + 231 + 232 + def reset_display_powersave_monitor() -> None: 233 + _MONITOR.reset()
+31 -5
solstone/think/processing.py
··· 69 69 70 70 71 71 @dataclass(frozen=True) 72 + class DisplayPowersaveReading: 73 + available: bool 74 + asleep: bool 75 + debounced: bool 76 + 77 + 78 + DISPLAY_POWERSAVE_UNAVAILABLE = DisplayPowersaveReading(False, False, False) 79 + 80 + 81 + @dataclass(frozen=True) 72 82 class ConditionState: 73 83 enabled: bool 74 84 available: bool ··· 203 213 return ConditionState(enabled=window.enabled, available=True, open=is_open) 204 214 205 215 206 - def evaluate_display_powersave(ps: DisplayPowersaveSettings) -> ConditionState: 207 - """Evaluate the display-powersave forward seam, currently unavailable.""" 208 - return ConditionState(enabled=ps.enabled, available=False, open=False) 216 + def evaluate_display_powersave( 217 + ps: DisplayPowersaveSettings, 218 + reading: DisplayPowersaveReading, 219 + ) -> ConditionState: 220 + """Evaluate the display-powersave condition from a debounced reading.""" 221 + if not ps.enabled: 222 + return ConditionState(enabled=False, available=False, open=False) 223 + return ConditionState( 224 + enabled=True, 225 + available=reading.available, 226 + open=reading.asleep and reading.debounced, 227 + ) 209 228 210 229 211 - def evaluate_drain_gate(settings: ProcessingSettings, now: datetime) -> GateState: 230 + def evaluate_drain_gate( 231 + settings: ProcessingSettings, 232 + now: datetime, 233 + display_reading: DisplayPowersaveReading, 234 + ) -> GateState: 212 235 """Evaluate all drain gate conditions with OR composition.""" 213 236 conditions = { 214 237 "time_window": evaluate_time_window(settings.gate.time_window, now), 215 238 "display_powersave": evaluate_display_powersave( 216 - settings.gate.display_powersave 239 + settings.gate.display_powersave, 240 + display_reading, 217 241 ), 218 242 } 219 243 return GateState( ··· 345 369 "DRAIN_STATE_WINDOW_OPEN", 346 370 "DEFAULT_PROCESSING", 347 371 "ConditionState", 372 + "DISPLAY_POWERSAVE_UNAVAILABLE", 373 + "DisplayPowersaveReading", 348 374 "DisplayPowersaveSettings", 349 375 "GateSettings", 350 376 "GateState",
+16 -2
solstone/think/supervisor.py
··· 39 39 record_attempt, 40 40 record_outcome, 41 41 ) 42 + from solstone.think.display_powersave import ( 43 + poll_display_powersave, 44 + reset_display_powersave_monitor, 45 + ) 42 46 from solstone.think.maint import run_pending_tasks 43 47 from solstone.think.models import LOCAL_MODEL, is_local_provider_needed 44 - from solstone.think.processing import evaluate_drain_gate, load_processing_settings 48 + from solstone.think.processing import ( 49 + DISPLAY_POWERSAVE_UNAVAILABLE, 50 + evaluate_drain_gate, 51 + load_processing_settings, 52 + ) 45 53 from solstone.think.providers.mlx_server import MLX_SERVER_PROCESS_NAME 46 54 from solstone.think.readiness import START_TIME_TOLERANCE_S, clear_ready, signal_ready 47 55 from solstone.think.runner import ManagedProcess as RunnerManagedProcess ··· 2592 2600 settings = load_processing_settings() 2593 2601 if settings.mode != "deferred": 2594 2602 return 2595 - gate = evaluate_drain_gate(settings, datetime.now()) 2603 + reading = ( 2604 + poll_display_powersave(time.monotonic()) 2605 + if settings.gate.display_powersave.enabled 2606 + else DISPLAY_POWERSAVE_UNAVAILABLE 2607 + ) 2608 + gate = evaluate_drain_gate(settings, datetime.now(), reading) 2596 2609 if not gate.open: 2597 2610 return 2598 2611 run_catchup_drain() ··· 2614 2627 2615 2628 last_status_emit = 0.0 2616 2629 _last_gate_tick = 0.0 2630 + reset_display_powersave_monitor() 2617 2631 _last_sync_tick = 0.0 2618 2632 _last_sync_snapshot = None 2619 2633 _sync_conflict_shutdown = False
+7 -1
solstone/think/surfaces/health.py
··· 22 22 unavailable_snapshot, 23 23 ) 24 24 from solstone.think.activities import load_activity_records 25 + from solstone.think.display_powersave import ( 26 + display_powersave_detectable, 27 + last_display_powersave, 28 + ) 25 29 from solstone.think.entities.journal import load_all_journal_entities 26 30 from solstone.think.facets import get_facets 27 31 from solstone.think.pipeline_health import read_segment_backlog ··· 507 511 1 for completion in backlog.per_day.values() if completion.not_thought > 0 508 512 ) 509 513 settings = load_processing_settings() 510 - gate = evaluate_drain_gate(settings, datetime.now()) 514 + reading = last_display_powersave() 515 + gate = evaluate_drain_gate(settings, datetime.now(), reading) 511 516 drain_state = derive_drain_state(settings, gate) 512 517 awaiting_total = backlog.not_sensed + backlog.not_thought 513 518 awaiting_text = ( ··· 523 528 awaiting_analysis_text=awaiting_text, 524 529 last_drained_at=read_last_drained_at(), 525 530 drain_state=drain_state, 531 + display_powersave_detectable=display_powersave_detectable(), 526 532 ) 527 533 528 534
+1
solstone/think/surfaces/types.py
··· 129 129 awaiting_analysis_text: str | None = None 130 130 last_drained_at: int | None = None 131 131 drain_state: str = DRAIN_STATE_REALTIME 132 + display_powersave_detectable: bool = False 132 133 133 134 134 135 @dataclass(frozen=True)
+382
tests/test_display_powersave.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import subprocess 7 + from pathlib import Path 8 + from unittest.mock import MagicMock 9 + 10 + import pytest 11 + 12 + from solstone.think import display_powersave as dp 13 + from solstone.think.processing import DISPLAY_POWERSAVE_UNAVAILABLE 14 + 15 + 16 + @pytest.fixture(autouse=True) 17 + def reset_display_monitor(): 18 + dp.reset_display_powersave_monitor() 19 + yield 20 + dp.reset_display_powersave_monitor() 21 + 22 + 23 + def _connector( 24 + root: Path, 25 + name: str, 26 + *, 27 + status: str | None, 28 + enabled: str | None, 29 + dpms: str | None, 30 + ) -> Path: 31 + path = root / name 32 + path.mkdir(parents=True) 33 + for attr_name, value in { 34 + "status": status, 35 + "enabled": enabled, 36 + "dpms": dpms, 37 + }.items(): 38 + if value is not None: 39 + (path / attr_name).write_text(value, encoding="utf-8") 40 + return path 41 + 42 + 43 + def _read_linux(root: Path): 44 + return dp.read_display_power(platform="linux", sysfs_root=root) 45 + 46 + 47 + def _reader(state: str, reason: str | None = None): 48 + return lambda: dp.DisplayReading(state, reason) 49 + 50 + 51 + def test_read_linux_returns_awake_when_any_considered_connector_is_on( 52 + tmp_path: Path, 53 + ) -> None: 54 + _connector( 55 + tmp_path, 56 + "card1-HDMI-A-2", 57 + status="connected", 58 + enabled="enabled", 59 + dpms="On", 60 + ) 61 + 62 + assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 63 + 64 + 65 + @pytest.mark.parametrize("dpms", ["Off", "Standby", "Suspend"]) 66 + def test_read_linux_returns_asleep_when_all_considered_connectors_are_off_states( 67 + tmp_path: Path, 68 + dpms: str, 69 + ) -> None: 70 + _connector( 71 + tmp_path, 72 + "card1-HDMI-A-2", 73 + status="connected", 74 + enabled="enabled", 75 + dpms=dpms, 76 + ) 77 + 78 + assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_ASLEEP, None) 79 + 80 + 81 + def test_read_linux_ignores_disconnected_and_disabled_stale_dpms( 82 + tmp_path: Path, 83 + ) -> None: 84 + _connector( 85 + tmp_path, 86 + "card1-HDMI-A-2", 87 + status="disconnected", 88 + enabled="enabled", 89 + dpms="On", 90 + ) 91 + _connector( 92 + tmp_path, 93 + "card1-DP-1", 94 + status="connected", 95 + enabled="disabled", 96 + dpms="On", 97 + ) 98 + 99 + assert _read_linux(tmp_path) == dp.DisplayReading( 100 + dp.DISPLAY_UNDETECTABLE, 101 + dp.REASON_HEADLESS, 102 + ) 103 + 104 + 105 + def test_read_linux_mixed_on_and_off_returns_awake(tmp_path: Path) -> None: 106 + _connector( 107 + tmp_path, 108 + "card1-HDMI-A-2", 109 + status="connected", 110 + enabled="enabled", 111 + dpms="Off", 112 + ) 113 + _connector( 114 + tmp_path, 115 + "card1-DP-1", 116 + status="connected", 117 + enabled="enabled", 118 + dpms="On", 119 + ) 120 + 121 + assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 122 + 123 + 124 + def test_read_linux_returns_unreadable_for_missing_or_unknown_considered_dpms( 125 + tmp_path: Path, 126 + ) -> None: 127 + _connector( 128 + tmp_path, 129 + "card1-HDMI-A-2", 130 + status="connected", 131 + enabled="enabled", 132 + dpms="Off", 133 + ) 134 + _connector( 135 + tmp_path, 136 + "card1-DP-1", 137 + status="connected", 138 + enabled="enabled", 139 + dpms=None, 140 + ) 141 + 142 + assert _read_linux(tmp_path) == dp.DisplayReading( 143 + dp.DISPLAY_UNDETECTABLE, 144 + dp.REASON_UNREADABLE, 145 + ) 146 + 147 + 148 + def test_read_linux_on_wins_over_unreadable_sibling(tmp_path: Path) -> None: 149 + _connector( 150 + tmp_path, 151 + "card1-HDMI-A-2", 152 + status="connected", 153 + enabled="enabled", 154 + dpms=None, 155 + ) 156 + _connector( 157 + tmp_path, 158 + "card1-DP-1", 159 + status="connected", 160 + enabled="enabled", 161 + dpms="On", 162 + ) 163 + 164 + assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 165 + 166 + 167 + def test_read_linux_returns_headless_when_no_connected_enabled_connectors( 168 + tmp_path: Path, 169 + ) -> None: 170 + _connector( 171 + tmp_path, 172 + "card1-HDMI-A-2", 173 + status="disconnected", 174 + enabled="enabled", 175 + dpms="Off", 176 + ) 177 + 178 + assert _read_linux(tmp_path) == dp.DisplayReading( 179 + dp.DISPLAY_UNDETECTABLE, 180 + dp.REASON_HEADLESS, 181 + ) 182 + 183 + 184 + def test_read_linux_returns_unreadable_when_no_connectors(tmp_path: Path) -> None: 185 + assert _read_linux(tmp_path) == dp.DisplayReading( 186 + dp.DISPLAY_UNDETECTABLE, 187 + dp.REASON_UNREADABLE, 188 + ) 189 + 190 + 191 + def test_read_linux_returns_unreadable_when_root_missing(tmp_path: Path) -> None: 192 + assert _read_linux(tmp_path / "missing") == dp.DisplayReading( 193 + dp.DISPLAY_UNDETECTABLE, 194 + dp.REASON_UNREADABLE, 195 + ) 196 + 197 + 198 + def test_read_linux_returns_unreadable_when_glob_raises() -> None: 199 + class BrokenRoot: 200 + def glob(self, _pattern): 201 + raise OSError("no drm") 202 + 203 + assert dp.read_display_power(platform="linux", sysfs_root=BrokenRoot()) == ( 204 + dp.DisplayReading(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNREADABLE) 205 + ) 206 + 207 + 208 + def test_read_display_power_wraps_detector_exception_as_unreadable( 209 + tmp_path: Path, 210 + monkeypatch: pytest.MonkeyPatch, 211 + ) -> None: 212 + _connector( 213 + tmp_path, 214 + "card1-HDMI-A-2", 215 + status="connected", 216 + enabled="enabled", 217 + dpms="On", 218 + ) 219 + 220 + def fail_read_attr(_path: Path, _name: str) -> str: 221 + raise RuntimeError("boom") 222 + 223 + monkeypatch.setattr(dp, "_read_attr", fail_read_attr) 224 + 225 + assert _read_linux(tmp_path) == dp.DisplayReading( 226 + dp.DISPLAY_UNDETECTABLE, 227 + dp.REASON_UNREADABLE, 228 + ) 229 + 230 + 231 + def test_read_display_power_dispatches_linux_and_darwin( 232 + tmp_path: Path, 233 + monkeypatch: pytest.MonkeyPatch, 234 + ) -> None: 235 + monkeypatch.setattr( 236 + dp, 237 + "_read_linux", 238 + lambda root: dp.DisplayReading(dp.DISPLAY_AWAKE, None), 239 + ) 240 + monkeypatch.setattr( 241 + dp, 242 + "_read_macos", 243 + lambda: dp.DisplayReading(dp.DISPLAY_ASLEEP, None), 244 + ) 245 + 246 + assert dp.read_display_power(platform="linux", sysfs_root=tmp_path).state == ( 247 + dp.DISPLAY_AWAKE 248 + ) 249 + assert dp.read_display_power(platform="darwin", sysfs_root=tmp_path).state == ( 250 + dp.DISPLAY_ASLEEP 251 + ) 252 + 253 + 254 + def test_read_display_power_returns_unsupported_platform(tmp_path: Path) -> None: 255 + assert dp.read_display_power(platform="win32", sysfs_root=tmp_path) == ( 256 + dp.DisplayReading(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNSUPPORTED_PLATFORM) 257 + ) 258 + 259 + 260 + def test_parse_macos_log_uses_last_display_marker() -> None: 261 + text = "\n".join( 262 + [ 263 + "2026-01-01 Display is turned off", 264 + "2026-01-01 Display is turned on", 265 + ] 266 + ) 267 + 268 + assert dp._parse_macos_log(text) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 269 + 270 + 271 + def test_parse_macos_log_without_marker_returns_no_window_server() -> None: 272 + assert dp._parse_macos_log("unrelated line") == dp.DisplayReading( 273 + dp.DISPLAY_UNDETECTABLE, 274 + dp.REASON_NO_WINDOW_SERVER, 275 + ) 276 + 277 + 278 + @pytest.mark.parametrize( 279 + ("asleep", "expected_state"), 280 + [(True, dp.DISPLAY_ASLEEP), (False, dp.DISPLAY_AWAKE)], 281 + ) 282 + def test_read_macos_uses_coregraphics_sleep_state_when_main_display_exists( 283 + monkeypatch: pytest.MonkeyPatch, 284 + asleep: bool, 285 + expected_state: str, 286 + ) -> None: 287 + monkeypatch.setattr(dp, "_macos_main_display_id", lambda: 1) 288 + monkeypatch.setattr(dp, "_macos_display_is_asleep", lambda display_id: asleep) 289 + 290 + assert dp._read_macos() == dp.DisplayReading(expected_state, None) 291 + 292 + 293 + def test_read_macos_falls_back_to_pmset_log_when_main_display_is_zero( 294 + monkeypatch: pytest.MonkeyPatch, 295 + ) -> None: 296 + monkeypatch.setattr(dp, "_macos_main_display_id", lambda: 0) 297 + monkeypatch.setattr(dp, "_macos_pmset_log", lambda: "Display is turned off") 298 + 299 + assert dp._read_macos() == dp.DisplayReading(dp.DISPLAY_ASLEEP, None) 300 + 301 + 302 + def test_macos_pmset_log_contains_timeout_and_oserror( 303 + monkeypatch: pytest.MonkeyPatch, 304 + ) -> None: 305 + def timeout(*_args, **_kwargs): 306 + raise subprocess.TimeoutExpired(["pmset", "-g", "log"], timeout=1) 307 + 308 + monkeypatch.setattr(dp.subprocess, "run", timeout) 309 + assert dp._macos_pmset_log() == "" 310 + 311 + def oserror(*_args, **_kwargs): 312 + raise OSError("pmset missing") 313 + 314 + monkeypatch.setattr(dp.subprocess, "run", oserror) 315 + assert dp._macos_pmset_log() == "" 316 + 317 + 318 + def test_monitor_debounces_only_after_threshold_from_injected_now() -> None: 319 + monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 320 + 321 + first = monitor.poll(now=0.0, read=_reader(dp.DISPLAY_ASLEEP)) 322 + before_threshold = monitor.poll(now=119.0, read=_reader(dp.DISPLAY_ASLEEP)) 323 + after_threshold = monitor.poll(now=240.0, read=_reader(dp.DISPLAY_ASLEEP)) 324 + 325 + assert first.debounced is False 326 + assert before_threshold.debounced is False 327 + assert after_threshold.debounced is True 328 + 329 + 330 + def test_monitor_wake_closes_immediately_and_resets_since() -> None: 331 + monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 332 + monitor.poll(now=0.0, read=_reader(dp.DISPLAY_ASLEEP)) 333 + monitor.poll(now=240.0, read=_reader(dp.DISPLAY_ASLEEP)) 334 + 335 + awake = monitor.poll(now=241.0, read=_reader(dp.DISPLAY_AWAKE)) 336 + next_asleep = monitor.poll(now=242.0, read=_reader(dp.DISPLAY_ASLEEP)) 337 + 338 + assert awake.available is True 339 + assert awake.asleep is False 340 + assert awake.debounced is False 341 + assert next_asleep.debounced is False 342 + 343 + 344 + def test_monitor_undetectable_resets_snapshot_and_debounce() -> None: 345 + monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 346 + monitor.poll(now=0.0, read=_reader(dp.DISPLAY_ASLEEP)) 347 + undetectable = monitor.poll( 348 + now=60.0, 349 + read=_reader(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNREADABLE), 350 + ) 351 + next_asleep = monitor.poll(now=180.0, read=_reader(dp.DISPLAY_ASLEEP)) 352 + 353 + assert undetectable == DISPLAY_POWERSAVE_UNAVAILABLE 354 + assert next_asleep.debounced is False 355 + 356 + 357 + def test_monitor_capability_known_is_sticky_until_reset() -> None: 358 + monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 359 + 360 + assert monitor.capability_known() is False 361 + monitor.poll(now=0.0, read=_reader(dp.DISPLAY_AWAKE)) 362 + assert monitor.capability_known() is True 363 + monitor.poll( 364 + now=1.0, 365 + read=_reader(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNREADABLE), 366 + ) 367 + assert monitor.capability_known() is True 368 + monitor.reset() 369 + assert monitor.capability_known() is False 370 + 371 + 372 + def test_singleton_last_and_reset_are_non_polling( 373 + monkeypatch: pytest.MonkeyPatch, 374 + ) -> None: 375 + snapshot = dp._MONITOR.poll(now=0.0, read=_reader(dp.DISPLAY_AWAKE)) 376 + read = MagicMock(side_effect=AssertionError("last should not poll")) 377 + monkeypatch.setattr(dp, "read_display_power", read) 378 + 379 + assert dp.last_display_powersave() == snapshot 380 + read.assert_not_called() 381 + dp.reset_display_powersave_monitor() 382 + assert dp.last_display_powersave() == DISPLAY_POWERSAVE_UNAVAILABLE
+1
tests/test_health_call_parity.py
··· 95 95 "awaiting_analysis_text": None, 96 96 "last_drained_at": None, 97 97 "drain_state": DRAIN_STATE_REALTIME, 98 + "display_powersave_detectable": False, 98 99 }, 99 100 "notes": [], 100 101 "provider_readiness": {
+57 -3
tests/test_processing.py
··· 12 12 from solstone.think.processing import ( 13 13 AWAITING_ANALYSIS_TEMPLATE, 14 14 DEFAULT_PROCESSING, 15 + DISPLAY_POWERSAVE_UNAVAILABLE, 15 16 DRAIN_STATE_NO_CONDITION, 16 17 DRAIN_STATE_REALTIME, 17 18 DRAIN_STATE_WAITING, 18 19 DRAIN_STATE_WINDOW_OPEN, 19 20 ConditionState, 21 + DisplayPowersaveReading, 20 22 DisplayPowersaveSettings, 21 23 GateSettings, 22 24 GateState, ··· 205 207 display_powersave=DisplayPowersaveSettings(enabled=True), 206 208 ), 207 209 datetime(2026, 1, 1, 3, 0), 210 + DISPLAY_POWERSAVE_UNAVAILABLE, 208 211 ).open 209 212 is True 210 213 ) ··· 216 219 display_powersave=DisplayPowersaveSettings(enabled=True), 217 220 ), 218 221 datetime(2026, 1, 1, 3, 0), 222 + DISPLAY_POWERSAVE_UNAVAILABLE, 219 223 ).open 220 224 is False 221 225 ) 222 226 223 227 224 - def test_evaluate_display_powersave_is_inert() -> None: 225 - state = evaluate_display_powersave(DisplayPowersaveSettings(enabled=True)) 228 + def test_evaluate_display_powersave_disabled_ignores_reading() -> None: 229 + state = evaluate_display_powersave( 230 + DisplayPowersaveSettings(enabled=False), 231 + DisplayPowersaveReading(available=True, asleep=True, debounced=True), 232 + ) 226 233 227 - assert state.enabled is True 234 + assert state.enabled is False 228 235 assert state.available is False 229 236 assert state.open is False 237 + 238 + 239 + def test_evaluate_display_powersave_enabled_uses_reading() -> None: 240 + asleep = evaluate_display_powersave( 241 + DisplayPowersaveSettings(enabled=True), 242 + DisplayPowersaveReading(available=True, asleep=True, debounced=True), 243 + ) 244 + awake = evaluate_display_powersave( 245 + DisplayPowersaveSettings(enabled=True), 246 + DisplayPowersaveReading(available=True, asleep=False, debounced=False), 247 + ) 248 + undetectable = evaluate_display_powersave( 249 + DisplayPowersaveSettings(enabled=True), 250 + DISPLAY_POWERSAVE_UNAVAILABLE, 251 + ) 252 + 253 + assert asleep == ConditionState(enabled=True, available=True, open=True) 254 + assert awake == ConditionState(enabled=True, available=True, open=False) 255 + assert undetectable == ConditionState(enabled=True, available=False, open=False) 256 + 257 + 258 + def test_evaluate_drain_gate_opens_on_debounced_display_powersave() -> None: 259 + gate = evaluate_drain_gate( 260 + _settings( 261 + time_window=_time_window(enabled=False), 262 + display_powersave=DisplayPowersaveSettings(enabled=True), 263 + ), 264 + datetime(2026, 1, 1, 12, 0), 265 + DisplayPowersaveReading(available=True, asleep=True, debounced=True), 266 + ) 267 + 268 + assert gate.open is True 269 + 270 + 271 + def test_display_powersave_undetectable_has_no_active_condition() -> None: 272 + settings = _settings( 273 + time_window=_time_window(enabled=False), 274 + display_powersave=DisplayPowersaveSettings(enabled=True), 275 + ) 276 + gate = evaluate_drain_gate( 277 + settings, 278 + datetime(2026, 1, 1, 12, 0), 279 + DISPLAY_POWERSAVE_UNAVAILABLE, 280 + ) 281 + 282 + assert gate.open is False 283 + assert derive_drain_state(settings, gate) == DRAIN_STATE_NO_CONDITION 230 284 231 285 232 286 def test_derive_drain_state_tokens() -> None:
+81 -5
tests/test_supervisor.py
··· 21 21 import pytest 22 22 23 23 from solstone.think.processing import ( 24 + DISPLAY_POWERSAVE_UNAVAILABLE, 25 + DisplayPowersaveReading, 24 26 DisplayPowersaveSettings, 25 27 GateSettings, 26 28 ProcessingSettings, ··· 626 628 self.submissions.append({"cmd": cmd, "day": day}) 627 629 628 630 629 - def _supervisor_processing_settings(mode: str) -> ProcessingSettings: 631 + def _supervisor_processing_settings( 632 + mode: str, 633 + *, 634 + display_powersave_enabled: bool = False, 635 + ) -> ProcessingSettings: 630 636 return ProcessingSettings( 631 637 mode=mode, 632 638 gate=GateSettings( ··· 635 641 start="02:00", 636 642 end="06:00", 637 643 ), 638 - display_powersave=DisplayPowersaveSettings(enabled=False), 644 + display_powersave=DisplayPowersaveSettings( 645 + enabled=display_powersave_enabled 646 + ), 639 647 ), 640 648 ) 641 649 ··· 1937 1945 monkeypatch.setattr( 1938 1946 mod, 1939 1947 "evaluate_drain_gate", 1940 - lambda settings, now: SimpleNamespace(open=True), 1948 + lambda settings, now, reading: SimpleNamespace(open=True), 1941 1949 ) 1942 1950 monkeypatch.setattr(mod, "run_catchup_drain", drain) 1943 1951 mod._last_gate_tick = 0.0 ··· 1958 1966 monkeypatch.setattr( 1959 1967 mod, 1960 1968 "evaluate_drain_gate", 1961 - lambda settings, now: SimpleNamespace(open=False), 1969 + lambda settings, now, reading: SimpleNamespace(open=False), 1962 1970 ) 1963 1971 monkeypatch.setattr(mod, "run_catchup_drain", drain) 1964 1972 mod._last_gate_tick = 0.0 ··· 1998 2006 monkeypatch.setattr( 1999 2007 mod, 2000 2008 "evaluate_drain_gate", 2001 - lambda settings, now: SimpleNamespace(open=True), 2009 + lambda settings, now, reading: SimpleNamespace(open=True), 2002 2010 ) 2003 2011 monkeypatch.setattr(mod, "run_catchup_drain", drain) 2004 2012 mod._last_gate_tick = 0.0 ··· 2008 2016 mod._run_gate_tick(121.0) 2009 2017 2010 2018 assert drain.call_count == 2 2019 + 2020 + 2021 + def test_run_gate_tick_disabled_display_powersave_does_not_poll(monkeypatch): 2022 + mod = importlib.import_module("solstone.think.supervisor") 2023 + drain = MagicMock() 2024 + poll = MagicMock(side_effect=AssertionError("poll should not be called")) 2025 + 2026 + def evaluate(settings, now, reading): 2027 + assert reading == DISPLAY_POWERSAVE_UNAVAILABLE 2028 + return SimpleNamespace(open=True) 2029 + 2030 + monkeypatch.setattr(mod, "_is_remote_mode", False) 2031 + monkeypatch.setattr( 2032 + mod, 2033 + "load_processing_settings", 2034 + lambda: _supervisor_processing_settings("deferred"), 2035 + ) 2036 + monkeypatch.setattr(mod, "poll_display_powersave", poll) 2037 + monkeypatch.setattr(mod, "evaluate_drain_gate", evaluate) 2038 + monkeypatch.setattr(mod, "run_catchup_drain", drain) 2039 + mod._last_gate_tick = 0.0 2040 + 2041 + mod._run_gate_tick(60.0) 2042 + 2043 + poll.assert_not_called() 2044 + drain.assert_called_once_with() 2045 + 2046 + 2047 + def test_run_gate_tick_enabled_display_powersave_polls(monkeypatch): 2048 + mod = importlib.import_module("solstone.think.supervisor") 2049 + reading = DisplayPowersaveReading(available=True, asleep=True, debounced=True) 2050 + poll = MagicMock(return_value=reading) 2051 + captured = {} 2052 + 2053 + def evaluate(settings, now, display_reading): 2054 + captured["reading"] = display_reading 2055 + return SimpleNamespace(open=False) 2056 + 2057 + monkeypatch.setattr(mod, "_is_remote_mode", False) 2058 + monkeypatch.setattr( 2059 + mod, 2060 + "load_processing_settings", 2061 + lambda: _supervisor_processing_settings( 2062 + "deferred", 2063 + display_powersave_enabled=True, 2064 + ), 2065 + ) 2066 + monkeypatch.setattr(mod, "poll_display_powersave", poll) 2067 + monkeypatch.setattr(mod, "evaluate_drain_gate", evaluate) 2068 + monkeypatch.setattr(mod, "run_catchup_drain", MagicMock()) 2069 + mod._last_gate_tick = 0.0 2070 + 2071 + mod._run_gate_tick(60.0) 2072 + 2073 + poll.assert_called_once() 2074 + assert isinstance(poll.call_args.args[0], float) 2075 + assert captured["reading"] == reading 2076 + 2077 + 2078 + def test_supervise_resets_display_powersave_monitor_on_entry(monkeypatch): 2079 + mod = importlib.import_module("solstone.think.supervisor") 2080 + reset = MagicMock() 2081 + monkeypatch.setattr(mod, "reset_display_powersave_monitor", reset) 2082 + monkeypatch.setattr(mod, "shutdown_requested", True) 2083 + 2084 + asyncio.run(mod.supervise(daily=False, schedule=False, procs=[])) 2085 + 2086 + reset.assert_called_once_with() 2011 2087 2012 2088 2013 2089 def test_record_scheduler_completion_serializes_concurrent_writes(
+104 -4
tests/test_surfaces_health.py
··· 8 8 import re 9 9 from datetime import UTC, datetime, timedelta 10 10 from pathlib import Path 11 + from unittest.mock import MagicMock 11 12 12 13 import pytest 13 14 from click.testing import Result ··· 17 18 from solstone.think.convey_client import ConveyClient 18 19 from solstone.think.pipeline_health import SegmentBacklog, SegmentCompletion 19 20 from solstone.think.processing import ( 21 + DISPLAY_POWERSAVE_UNAVAILABLE, 20 22 DRAIN_STATE_NO_CONDITION, 21 23 DRAIN_STATE_REALTIME, 22 24 DRAIN_STATE_WAITING, 23 25 DRAIN_STATE_WINDOW_OPEN, 24 26 ConditionState, 27 + DisplayPowersaveReading, 25 28 DisplayPowersaveSettings, 26 29 GateSettings, 27 30 GateState, ··· 243 246 ) 244 247 245 248 249 + def _stub_display_powersave( 250 + monkeypatch: pytest.MonkeyPatch, 251 + *, 252 + reading=DISPLAY_POWERSAVE_UNAVAILABLE, 253 + detectable: bool = False, 254 + ) -> None: 255 + monkeypatch.setattr(health_surface, "last_display_powersave", lambda: reading) 256 + monkeypatch.setattr( 257 + health_surface, 258 + "display_powersave_detectable", 259 + lambda: detectable, 260 + ) 261 + 262 + 246 263 def _clear_readiness_snapshot() -> dict[str, object]: 247 264 return { 248 265 "summary": { ··· 586 603 monkeypatch.setattr( 587 604 health_surface, 588 605 "evaluate_drain_gate", 589 - lambda settings, now: GateState(open=False, conditions={}), 606 + lambda settings, now, reading: GateState(open=False, conditions={}), 590 607 ) 591 608 monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 609 + _stub_display_powersave(monkeypatch) 592 610 593 611 backlog = health_surface._build_segment_backlog_health() 594 612 ··· 611 629 monkeypatch.setattr( 612 630 health_surface, 613 631 "evaluate_drain_gate", 614 - lambda settings, now: GateState(open=True, conditions={}), 632 + lambda settings, now, reading: GateState(open=True, conditions={}), 615 633 ) 616 634 monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 635 + _stub_display_powersave(monkeypatch) 617 636 618 637 backlog = health_surface._build_segment_backlog_health() 619 638 ··· 664 683 monkeypatch.setattr( 665 684 health_surface, 666 685 "evaluate_drain_gate", 667 - lambda settings, now: gate, 686 + lambda settings, now, reading: gate, 668 687 ) 669 688 monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 689 + _stub_display_powersave(monkeypatch) 670 690 671 691 backlog = health_surface._build_segment_backlog_health() 672 692 ··· 687 707 monkeypatch.setattr( 688 708 health_surface, 689 709 "evaluate_drain_gate", 690 - lambda settings, now: GateState(open=False, conditions={}), 710 + lambda settings, now, reading: GateState(open=False, conditions={}), 691 711 ) 692 712 monkeypatch.setattr( 693 713 health_surface, 694 714 "read_last_drained_at", 695 715 lambda: 1_700_000_000_000, 696 716 ) 717 + _stub_display_powersave(monkeypatch) 697 718 698 719 backlog = health_surface._build_segment_backlog_health() 699 720 700 721 assert backlog.last_drained_at == 1_700_000_000_000 722 + 723 + 724 + def test_segment_backlog_uses_last_display_powersave_snapshot(monkeypatch) -> None: 725 + expected = DisplayPowersaveReading(available=True, asleep=True, debounced=True) 726 + captured = {} 727 + monkeypatch.setattr( 728 + health_surface, 729 + "read_segment_backlog", 730 + lambda: _segment_backlog({}), 731 + ) 732 + monkeypatch.setattr( 733 + health_surface, 734 + "load_processing_settings", 735 + lambda: _processing_settings("deferred"), 736 + ) 737 + 738 + def evaluate(settings, now, reading): 739 + captured["reading"] = reading 740 + return GateState(open=True, conditions={}) 741 + 742 + monkeypatch.setattr(health_surface, "evaluate_drain_gate", evaluate) 743 + monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 744 + _stub_display_powersave(monkeypatch, reading=expected) 745 + 746 + health_surface._build_segment_backlog_health() 747 + 748 + assert captured["reading"] == expected 749 + 750 + 751 + def test_segment_backlog_exposes_display_powersave_detectable(monkeypatch) -> None: 752 + monkeypatch.setattr( 753 + health_surface, 754 + "read_segment_backlog", 755 + lambda: _segment_backlog({}), 756 + ) 757 + monkeypatch.setattr( 758 + health_surface, 759 + "load_processing_settings", 760 + lambda: _processing_settings("deferred"), 761 + ) 762 + monkeypatch.setattr( 763 + health_surface, 764 + "evaluate_drain_gate", 765 + lambda settings, now, reading: GateState(open=False, conditions={}), 766 + ) 767 + monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 768 + _stub_display_powersave(monkeypatch, detectable=True) 769 + 770 + backlog = health_surface._build_segment_backlog_health() 771 + 772 + assert backlog.display_powersave_detectable is True 773 + 774 + 775 + def test_segment_backlog_health_never_polls_display(monkeypatch) -> None: 776 + from solstone.think import display_powersave 777 + 778 + poll = MagicMock() 779 + monkeypatch.setattr(display_powersave, "poll_display_powersave", poll) 780 + monkeypatch.setattr( 781 + health_surface, 782 + "read_segment_backlog", 783 + lambda: _segment_backlog({}), 784 + ) 785 + monkeypatch.setattr( 786 + health_surface, 787 + "load_processing_settings", 788 + lambda: _processing_settings("deferred"), 789 + ) 790 + monkeypatch.setattr( 791 + health_surface, 792 + "evaluate_drain_gate", 793 + lambda settings, now, reading: GateState(open=False, conditions={}), 794 + ) 795 + monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 796 + _stub_display_powersave(monkeypatch) 797 + 798 + health_surface._build_segment_backlog_health() 799 + 800 + poll.assert_not_called() 701 801 702 802 703 803 def test_silent_facet_note_ladder_thresholds(tmp_path, monkeypatch):