personal memory agent
0

Configure Feed

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

feat(surfaces): report an honest no-thinking-engine state

Add DRAIN_STATE_NO_ENGINE, taking precedence over the realtime/deferred mode check so the state is honest in both modes.

Add the thinking_engine_not_chosen readiness reason in all four registries: Python reason codes, presenter entries, chat-reasons JS, and health JS. The no-engine state is intercepted before cloud readiness so it never collapses into provider_key_missing.

generate consumers that previously degraded on a missing API key now degrade identically on no-brain rather than crashing: transcript segmentation/JSON, creation-time detection, depict, enrich, and describe. Import and capture keep working; only analysis defers.

+229 -31
+1
solstone/apps/chat/copy.py
··· 67 67 # but not yet analyzed"). Backend-only — no chat_copy.js twin. Substance locked 68 68 # (wording VPX-refinable). Copy-canon compliant: no surveillance verbs. 69 69 CHAT_DEFERRED_NOT_ANALYZED = "Today's segments aren't analyzed yet — they'll process during your deferred window." 70 + CHAT_THINKING_ENGINE_NOT_CHOSEN = "No thinking engine is chosen yet. Choose one in Thinking so I can answer from your observations." 70 71 # fmt: on 71 72 72 73 from typing import Literal
+1
solstone/apps/health/static/health.js
··· 452 452 blocker: 3, 453 453 }; 454 454 const PROVIDER_LEVEL_REASON_CODES = new Set([ 455 + 'thinking_engine_not_chosen', 455 456 'provider_key_missing', 456 457 'provider_key_invalid', 457 458 'provider_quota_exceeded',
+1
solstone/apps/thinking/copy.py
··· 9 9 10 10 HEADING = "thinking" 11 11 ACTIVE_LANE_LABELS = { 12 + "none": "No provider chosen", 12 13 "scout": "Scout", 13 14 "byo": "BYO cloud", 14 15 "local": "Local",
+10 -2
solstone/apps/thinking/routes.py
··· 45 45 read_journal_config, 46 46 write_journal_config, 47 47 ) 48 - from solstone.think.models import LOCAL_MODEL, TYPE_DEFAULTS 48 + from solstone.think.models import ( 49 + LOCAL_MODEL, 50 + NO_BRAIN_PROVIDER, 51 + TYPE_DEFAULTS, 52 + resolve_provider, 53 + ) 49 54 from solstone.think.providers import ( 50 55 PROVIDER_REGISTRY, 51 56 build_provider_status, ··· 203 208 type_config = providers_config.get(agent_type, {}) 204 209 if not isinstance(type_config, dict): 205 210 type_config = {} 211 + provider, _ = resolve_provider("", agent_type) 206 212 settings[agent_type] = { 207 - "provider": type_config.get("provider", defaults["provider"]), 213 + "provider": provider, 208 214 "tier": type_config.get("tier", defaults["tier"]), 209 215 "backup": type_config.get("backup", defaults["backup"]), 210 216 } ··· 212 218 213 219 214 220 def _lane_for_provider(provider: str) -> str: 221 + if provider == NO_BRAIN_PROVIDER: 222 + return "none" 215 223 if provider == "local": 216 224 return "local" 217 225 if provider == "google" and scout.is_scout_enabled():
+33 -2
solstone/apps/thinking/tests/test_providers_payload_extended.py
··· 11 11 from solstone.apps.thinking import routes 12 12 from solstone.apps.thinking.local_bootstrap import LOCAL_MODEL_SPECS 13 13 from solstone.convey import create_app 14 - from solstone.think.models import LOCAL_MODEL 14 + from solstone.think.models import LOCAL_MODEL, NO_BRAIN_PROVIDER 15 15 from solstone.think.providers.install_state import InstallState 16 16 from solstone.think.providers.state import ProviderState 17 17 ··· 158 158 assert response.status_code == 200 159 159 payload = response.get_json() 160 160 assert payload["active_lane"]["lane"] == "byo" 161 + assert payload["active_lane"]["split"] is False 162 + 163 + 164 + def test_get_providers_reports_none_lane_when_no_engine_selected( 165 + settings_env, 166 + monkeypatch, 167 + ): 168 + journal_path, config = settings_env( 169 + { 170 + "setup": {"completed_at": "2026-05-23T00:00:00Z"}, 171 + "env": {}, 172 + "providers": {"contexts": {}, "models": {}}, 173 + } 174 + ) 175 + monkeypatch.setattr( 176 + "solstone.think.providers.state.local_runtime_ready", lambda: False 177 + ) 178 + client, _journal_path = _settings_client_with_journal( 179 + lambda: (journal_path, config) 180 + ) 181 + 182 + response = client.get("/app/thinking/api/providers") 183 + 184 + assert response.status_code == 200 185 + payload = response.get_json() 186 + assert payload["active_lane"]["lane"] == NO_BRAIN_PROVIDER 161 187 assert payload["active_lane"]["split"] is False 162 188 163 189 ··· 549 575 cloud_pin = effective_contexts["talent.cloud.pin"] 550 576 assert cloud_pin["interface"] == "generate" 551 577 assert cloud_pin["provider"] == "local" 578 + assert cloud_pin["model"] == LOCAL_MODEL 552 579 assert cloud_pin["differs_from_raw"] is True 553 580 blank_pin = effective_contexts["talent.local.blank"] 554 581 assert blank_pin["provider"] == "local" ··· 567 594 "provider": "google", 568 595 "model": "gemini-flash-lite-latest", 569 596 } 597 + config["providers"]["contexts"]["talent.local.blank"] = { 598 + "provider": "local", 599 + "model": "", 600 + } 570 601 _write_config(journal_path, config) 571 602 572 603 def fake_readiness(provider: str, interface: str, model: str): ··· 586 617 587 618 assert response.status_code == 200 588 619 context_routes = response.get_json()["ai_readiness"]["context_routes"] 589 - assert any(route["provider"] == "local" for route in context_routes) 620 + assert {route["provider"] for route in context_routes} == {"local"} 590 621 591 622 592 623 def test_get_providers_ai_readiness_surfaces_gpu_probe_failed_from_inspect(
+17 -3
solstone/convey/chat.py
··· 39 39 CHAT_SUPPORT_SUBMIT_AMBIGUOUS, 40 40 CHAT_SUPPORT_SUBMIT_FAILED, 41 41 CHAT_SUPPORT_SUBMIT_FILED_FORMAT, 42 + CHAT_THINKING_ENGINE_NOT_CHOSEN, 42 43 ) 43 44 from solstone.apps.support.tools import support_attach, support_create, support_reply 44 45 from solstone.convey.chat_sources import parse_sol_sources ··· 555 556 _emit_cortex_event(message["event"], **fields) 556 557 557 558 559 + def _no_thinking_engine_chosen() -> bool: 560 + from solstone.think.models import no_thinking_engine_chosen 561 + 562 + return no_thinking_engine_chosen() 563 + 564 + 558 565 def compose_honest_degradation( 559 566 settings: ProcessingSettings, 560 567 backlog: SegmentBacklog, ··· 571 578 fire requires pending > 0; it is derived from the real backlog read, never 572 579 fabricated. 573 580 """ 574 - if settings.mode != "deferred": 581 + no_engine = _no_thinking_engine_chosen() 582 + if not no_engine and settings.mode != "deferred": 575 583 return None 576 584 if backlog.errors: 577 - return None 585 + return CHAT_THINKING_ENGINE_NOT_CHOSEN if no_engine else None 578 586 anchor_day = queried_day if queried_day is not None else _today_day() 579 587 completion = backlog.per_day.get(anchor_day) 580 588 if completion is None: 581 - return None 589 + return CHAT_THINKING_ENGINE_NOT_CHOSEN if no_engine else None 582 590 pending = completion.not_sensed + completion.not_thought 591 + if no_engine: 592 + if pending > 0: 593 + return ( 594 + f"{CHAT_THINKING_ENGINE_NOT_CHOSEN} {format_awaiting_analysis(pending)}" 595 + ) 596 + return CHAT_THINKING_ENGINE_NOT_CHOSEN 583 597 if pending <= 0: 584 598 return None 585 599 return f"{CHAT_DEFERRED_NOT_ANALYZED} {format_awaiting_analysis(pending)}"
+11
solstone/convey/provider_readiness.py
··· 53 53 54 54 PROVIDER_LEVEL_CODES = frozenset( 55 55 { 56 + "thinking_engine_not_chosen", 56 57 "provider_key_missing", 57 58 "provider_key_invalid", 58 59 "provider_quota_exceeded", ··· 70 71 label="Open Settings", 71 72 target="/app/thinking/#main", 72 73 ) 74 + _THINKING_ACTION = RecoveryAction( 75 + label="Open Thinking", 76 + target="/app/thinking/#main", 77 + ) 73 78 _LOCAL_SETUP_ACTION = RecoveryAction( 74 79 label="Open Local Model Setup", 75 80 target="/app/thinking/#local-setup", ··· 81 86 ) 82 87 83 88 _ENTRIES: dict[str, _Entry] = { 89 + "thinking_engine_not_chosen": _Entry( 90 + klass="setup", 91 + summary="no thinking engine is chosen yet", 92 + detail="Open Thinking to choose how sol thinks, then try again.", 93 + recovery_action=_THINKING_ACTION, 94 + ), 84 95 "provider_key_missing": _Entry( 85 96 klass="setup", 86 97 summary="{provider} needs credentials before it can read your screen descriptions",
+4
solstone/convey/static/chat_reasons.js
··· 10 10 }); 11 11 12 12 const CHAT_REASONS = Object.freeze({ 13 + "thinking_engine_not_chosen": { 14 + "template": "no thinking engine is chosen yet", 15 + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} 16 + }, 13 17 "provider_key_missing": { 14 18 "template": "{provider} needs credentials before it can read your screen descriptions", 15 19 "action": {"label": "Open Settings", "href": "/app/thinking/#main"}
+11 -4
solstone/observe/depict.py
··· 15 15 from solstone.observe.detect import detect_objects, detections_block 16 16 from solstone.observe.utils import get_segment_key, resize_for_vlm 17 17 from solstone.think.journal_io import write_jsonl 18 - from solstone.think.models import generate 18 + from solstone.think.models import NoBrainConfiguredError, generate 19 19 from solstone.think.utils import require_solstone, setup_cli 20 20 21 21 logger = logging.getLogger(__name__) ··· 57 57 img.save(buf, format="PNG") 58 58 detect_png = buf.getvalue() 59 59 prepared = resize_for_vlm(img) 60 - description = generate( 61 - contents=[_DESCRIBE_PROMPT, prepared], context="observe.depict" 62 - ).strip() 60 + try: 61 + description = generate( 62 + contents=[_DESCRIBE_PROMPT, prepared], context="observe.depict" 63 + ).strip() 64 + except NoBrainConfiguredError: 65 + logger.info( 66 + "No thinking engine chosen; deferring still-image description for %s", 67 + image_path, 68 + ) 69 + return None 63 70 64 71 header = _build_header(image_path.name, "image") 65 72 entry = {"start": "00:00:00", "text": description}
+14 -3
solstone/observe/describe.py
··· 695 695 Path to write JSONL output (when None, no output file is written) 696 696 """ 697 697 from solstone.think.batch import Batch 698 - from solstone.think.models import resolve_provider 698 + from solstone.think.models import NO_BRAIN_PROVIDER, resolve_provider 699 699 700 700 # Load config for max_extractions and redaction rules 701 701 config = get_config() ··· 748 748 749 749 try: 750 750 # Resolve model for frame description (tier from describe.md frontmatter) 751 - _, frame_model = resolve_provider(FRAME_CONTEXT, "generate") 751 + frame_provider, frame_model = resolve_provider(FRAME_CONTEXT, "generate") 752 + if frame_provider == NO_BRAIN_PROVIDER: 753 + logger.info("No thinking engine selected; deferring frame description") 754 + return 752 755 753 756 # Create vision requests for all qualified frames 754 757 for frame_data in qualified_frames: ··· 1068 1071 is_json = cat_meta.get("output") == "json" 1069 1072 1070 1073 # Resolve model for this category context 1071 - _, cat_model = resolve_provider(cat_meta["context"], "generate") 1074 + cat_provider, cat_model = resolve_provider( 1075 + cat_meta["context"], "generate" 1076 + ) 1077 + if cat_provider == NO_BRAIN_PROVIDER: 1078 + logger.info( 1079 + "No thinking engine selected; deferring %s extraction", 1080 + category, 1081 + ) 1082 + continue 1072 1083 1073 1084 batch.update( 1074 1085 extract_req,
+4 -1
solstone/observe/enrich.py
··· 22 22 from google.genai import types 23 23 24 24 from solstone.observe.utils import audio_to_flac_bytes 25 - from solstone.think.models import generate 25 + from solstone.think.models import NoBrainConfiguredError, generate 26 26 from solstone.think.prompts import load_prompt 27 27 28 28 logger = logging.getLogger(__name__) ··· 145 145 146 146 return result 147 147 148 + except NoBrainConfiguredError: 149 + logger.info("No thinking engine chosen; audio enrichment skipped") 150 + return None 148 151 except Exception as e: 149 152 logger.warning(f"Enrichment failed: {e}") 150 153 from solstone.think.models import IncompleteJSONError
+8 -1
solstone/think/detect_created.py
··· 6 6 from __future__ import annotations 7 7 8 8 import json 9 + import logging 9 10 import re 10 11 import subprocess 11 12 from datetime import datetime, timezone ··· 46 47 _SCHEMA = json.loads( 47 48 (Path(__file__).parent / "detect_created.schema.json").read_text(encoding="utf-8") 48 49 ) 50 + logger = logging.getLogger(__name__) 49 51 50 52 51 53 def _load_system_prompt() -> str: ··· 263 265 if guidance: 264 266 markdown += f"\n\nImportant guidance from the user: {guidance}" 265 267 266 - from solstone.think.models import generate 268 + from solstone.think.models import NoBrainConfiguredError, generate 267 269 268 270 try: 269 271 response_text = generate( ··· 277 279 json_schema=_SCHEMA, 278 280 ) 279 281 result = json.loads(response_text) 282 + except NoBrainConfiguredError: 283 + logger.info( 284 + "No thinking engine chosen; creation timestamp model detection skipped" 285 + ) 286 + return None 280 287 except (ValueError, json.JSONDecodeError): 281 288 return None 282 289
+8 -2
solstone/think/detect_transcript.py
··· 151 151 contents = f"START_TIME: {start_time}\n{numbered}" 152 152 logging.info(f"Starting transcript segmentation (start: {start_time})...") 153 153 154 - from solstone.think.models import generate 154 + from solstone.think.models import NoBrainConfiguredError, generate 155 155 156 156 try: 157 157 response_text = generate( ··· 170 170 segments = segments_from_boundaries(lines, boundaries) 171 171 172 172 return segments 173 + except NoBrainConfiguredError: 174 + logging.info("No thinking engine chosen; transcript segmentation skipped") 175 + return [] 173 176 except (ValueError, json.JSONDecodeError) as e: 174 177 logging.error(f"Transcript segmentation failed: {e}") 175 178 return [] ··· 192 195 # Prepend SEGMENT_START for the prompt 193 196 contents = f"SEGMENT_START: {segment_start}\n{text}" 194 197 195 - from solstone.think.models import generate 198 + from solstone.think.models import NoBrainConfiguredError, generate 196 199 197 200 try: 198 201 response_text = generate( ··· 210 213 result = json.loads(response_text) 211 214 logging.info("Successfully converted transcript to JSON") 212 215 return result 216 + except NoBrainConfiguredError: 217 + logging.info("No thinking engine chosen; transcript JSON conversion skipped") 218 + return None 213 219 except (ValueError, json.JSONDecodeError) as e: 214 220 logging.error(f"Failed to parse JSON response from LLM: {e}") 215 221 return None
+5
solstone/think/processing.py
··· 22 22 DRAIN_STATE_WINDOW_OPEN = "window_open" 23 23 DRAIN_STATE_WAITING = "waiting_for_window" 24 24 DRAIN_STATE_NO_CONDITION = "no_active_condition" 25 + DRAIN_STATE_NO_ENGINE = "no_engine" 25 26 26 27 _MISSING = object() 27 28 _MODES = frozenset({"realtime", "deferred"}) ··· 273 274 def derive_drain_state( 274 275 settings: ProcessingSettings, 275 276 gate_state: GateState, 277 + no_engine: bool, 276 278 ) -> str: 277 279 """Return the stable drain-state token for current settings and gate state.""" 280 + if no_engine: 281 + return DRAIN_STATE_NO_ENGINE 278 282 if settings.mode != "deferred": 279 283 return DRAIN_STATE_REALTIME 280 284 if gate_state.open: ··· 364 368 __all__ = [ 365 369 "AWAITING_ANALYSIS_TEMPLATE", 366 370 "DRAIN_STATE_NO_CONDITION", 371 + "DRAIN_STATE_NO_ENGINE", 367 372 "DRAIN_STATE_REALTIME", 368 373 "DRAIN_STATE_WAITING", 369 374 "DRAIN_STATE_WINDOW_OPEN",
+14 -6
solstone/think/surfaces/health.py
··· 57 57 # 7d matches the weekly freshness bar for search-backed consumers; shorter windows would over-warn on journals that intentionally rebuild less often. 58 58 LEDGER_STALE_DAYS = 14 59 59 # 14d mirrors the consumer-signal stale-item threshold so the health surface stays aligned with ledger backlog review. 60 + NO_ENGINE_ANALYSIS_TEXT = ( 61 + "No thinking engine is chosen yet. Choose one in Thinking so observations " 62 + "can be analyzed." 63 + ) 60 64 USER_EDIT_ACTOR_PREFIXES = ("cli:", "owner", "user") 61 65 # These prefixes identify operator- or user-authored corrections without trying to enumerate every internal automation actor string. 62 66 DEGRADED_OUTPUT_NOTE_CAP = 10 ··· 513 517 settings = load_processing_settings() 514 518 reading = last_display_powersave() 515 519 gate = evaluate_drain_gate(settings, datetime.now(), reading) 516 - drain_state = derive_drain_state(settings, gate) 520 + from solstone.think.models import no_thinking_engine_chosen 521 + 522 + no_engine = no_thinking_engine_chosen() 523 + drain_state = derive_drain_state(settings, gate, no_engine) 517 524 awaiting_total = backlog.not_sensed + backlog.not_thought 518 - awaiting_text = ( 519 - format_awaiting_analysis(awaiting_total) 520 - if settings.mode == "deferred" 521 - else None 522 - ) 525 + if no_engine: 526 + awaiting_text = NO_ENGINE_ANALYSIS_TEXT 527 + elif settings.mode == "deferred": 528 + awaiting_text = format_awaiting_analysis(awaiting_total) 529 + else: 530 + awaiting_text = None 523 531 return SegmentBacklogHealth( 524 532 not_thought=backlog.not_thought, 525 533 days_with_backlog=days_with_backlog,
+1
tests/test_chat_reasons.py
··· 12 12 13 13 EXPECTED_CODES = { 14 14 "provider_key_missing", 15 + "thinking_engine_not_chosen", 15 16 "ram_insufficient", 16 17 "gpu_unavailable", 17 18 "gpu_probe_failed",
+27 -1
tests/test_convey_chat_deferred.py
··· 8 8 import pytest 9 9 from flask import Flask 10 10 11 - from solstone.apps.chat.copy import CHAT_DEFERRED_NOT_ANALYZED 11 + from solstone.apps.chat.copy import ( 12 + CHAT_DEFERRED_NOT_ANALYZED, 13 + CHAT_THINKING_ENGINE_NOT_CHOSEN, 14 + ) 12 15 from solstone.convey.chat import chat_bp, compose_honest_degradation 13 16 from solstone.convey.chat_stream import read_chat_events 14 17 from solstone.think.pipeline_health import SegmentBacklog, SegmentCompletion ··· 39 42 timer.cancel() 40 43 chat_module._watchdog_timers.clear() 41 44 chat_module._last_use_id = 0 45 + 46 + 47 + @pytest.fixture(autouse=True) 48 + def _default_thinking_engine_selected(monkeypatch): 49 + monkeypatch.setattr( 50 + "solstone.convey.chat._no_thinking_engine_chosen", lambda: False 51 + ) 42 52 43 53 44 54 def _set_current_chat(chat_module, logical_use_id: str, raw_use_id: str | None) -> None: ··· 226 236 ) 227 237 228 238 assert result is None 239 + 240 + 241 + def test_compose_honest_degradation_fires_for_no_engine_realtime(monkeypatch): 242 + import solstone.convey.chat as chat 243 + 244 + monkeypatch.setattr(chat, "_no_thinking_engine_chosen", lambda: True) 245 + today = chat._today_day() 246 + 247 + result = compose_honest_degradation( 248 + _settings("realtime"), 249 + _backlog(today, not_sensed=2, not_thought=1), 250 + ) 251 + 252 + assert result is not None 253 + assert CHAT_THINKING_ENGINE_NOT_CHOSEN in result 254 + assert format_awaiting_analysis(3) in result 229 255 230 256 231 257 def test_empty_chat_finish_substitutes_honest_degradation(chat_client, monkeypatch):
+1
tests/test_health_readiness_js.py
··· 76 76 ) 77 77 for code, provider, model in ( 78 78 ("provider_key_missing", "anthropic", "claude-test"), 79 + ("thinking_engine_not_chosen", "none", ""), 79 80 ("provider_quota_exceeded", "openai", "gpt-test"), 80 81 ("local_model_missing", "local", "qwen-test"), 81 82 ("local_server_unhealthy", "local", "qwen-test"),
+9 -5
tests/test_processing.py
··· 14 14 DEFAULT_PROCESSING, 15 15 DISPLAY_POWERSAVE_UNAVAILABLE, 16 16 DRAIN_STATE_NO_CONDITION, 17 + DRAIN_STATE_NO_ENGINE, 17 18 DRAIN_STATE_REALTIME, 18 19 DRAIN_STATE_WAITING, 19 20 DRAIN_STATE_WINDOW_OPEN, ··· 280 281 ) 281 282 282 283 assert gate.open is False 283 - assert derive_drain_state(settings, gate) == DRAIN_STATE_NO_CONDITION 284 + assert derive_drain_state(settings, gate, False) == DRAIN_STATE_NO_CONDITION 284 285 285 286 286 287 def test_derive_drain_state_tokens() -> None: ··· 300 301 }, 301 302 ) 302 303 303 - assert derive_drain_state(_settings(mode="realtime"), open_gate) == ( 304 + assert derive_drain_state(_settings(mode="realtime"), open_gate, False) == ( 304 305 DRAIN_STATE_REALTIME 305 306 ) 306 - assert derive_drain_state(_settings(mode="deferred"), open_gate) == ( 307 + assert derive_drain_state(_settings(mode="deferred"), open_gate, False) == ( 307 308 DRAIN_STATE_WINDOW_OPEN 308 309 ) 309 - assert derive_drain_state(_settings(mode="deferred"), waiting_gate) == ( 310 + assert derive_drain_state(_settings(mode="deferred"), waiting_gate, False) == ( 310 311 DRAIN_STATE_WAITING 311 312 ) 312 - assert derive_drain_state(_settings(mode="deferred"), unavailable_gate) == ( 313 + assert derive_drain_state(_settings(mode="deferred"), unavailable_gate, False) == ( 313 314 DRAIN_STATE_NO_CONDITION 315 + ) 316 + assert derive_drain_state(_settings(mode="realtime"), open_gate, True) == ( 317 + DRAIN_STATE_NO_ENGINE 314 318 ) 315 319 316 320
+2
tests/test_provider_readiness_presenter.py
··· 85 85 provider_level = semantic_key_for( 86 86 "provider_key_missing", "anthropic", "claude-test" 87 87 ) 88 + no_engine = semantic_key_for("thinking_engine_not_chosen", "none", "") 88 89 model_level = semantic_key_for("local_model_missing", "local", "llama-test") 89 90 90 91 assert provider_level == "provider_key_missing:anthropic:" 92 + assert no_engine == "thinking_engine_not_chosen:none:" 91 93 assert semantic_key_for("provider_key_missing", "anthropic", "other") == ( 92 94 provider_level 93 95 )
+11 -1
tests/test_provider_state.py
··· 7 7 8 8 import pytest 9 9 10 - from solstone.think.models import LOCAL_MODEL, QWEN_35_9B 10 + from solstone.think.models import LOCAL_MODEL, NO_BRAIN_PROVIDER, QWEN_35_9B 11 11 from solstone.think.providers import ( 12 12 local_endpoint, 13 13 local_install, ··· 244 244 245 245 assert provider_state.status == "blocked" 246 246 assert provider_state.reason_code == "provider_key_missing" 247 + assert provider_state.source == "config" 248 + 249 + 250 + def test_no_brain_readiness_is_not_key_missing(monkeypatch): 251 + monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: False) 252 + 253 + provider_state = state.readiness_for_provider(NO_BRAIN_PROVIDER, "generate", "") 254 + 255 + assert provider_state.status == "blocked" 256 + assert provider_state.reason_code == "thinking_engine_not_chosen" 247 257 assert provider_state.source == "config" 248 258 249 259
+36
tests/test_surfaces_health.py
··· 21 21 from solstone.think.processing import ( 22 22 DISPLAY_POWERSAVE_UNAVAILABLE, 23 23 DRAIN_STATE_NO_CONDITION, 24 + DRAIN_STATE_NO_ENGINE, 24 25 DRAIN_STATE_REALTIME, 25 26 DRAIN_STATE_WAITING, 26 27 DRAIN_STATE_WINDOW_OPEN, ··· 608 609 lambda settings, now, reading: GateState(open=False, conditions={}), 609 610 ) 610 611 monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 612 + monkeypatch.setattr( 613 + "solstone.think.models.no_thinking_engine_chosen", lambda: False 614 + ) 611 615 _stub_display_powersave(monkeypatch) 612 616 613 617 backlog = health_surface._build_segment_backlog_health() ··· 634 638 lambda settings, now, reading: GateState(open=True, conditions={}), 635 639 ) 636 640 monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 641 + monkeypatch.setattr( 642 + "solstone.think.models.no_thinking_engine_chosen", lambda: False 643 + ) 637 644 _stub_display_powersave(monkeypatch) 638 645 639 646 backlog = health_surface._build_segment_backlog_health() ··· 688 695 lambda settings, now, reading: gate, 689 696 ) 690 697 monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 698 + monkeypatch.setattr( 699 + "solstone.think.models.no_thinking_engine_chosen", lambda: False 700 + ) 691 701 _stub_display_powersave(monkeypatch) 692 702 693 703 backlog = health_surface._build_segment_backlog_health() 694 704 695 705 assert backlog.drain_state == expected_state 706 + 707 + 708 + def test_segment_backlog_no_engine_wins_before_realtime(monkeypatch) -> None: 709 + monkeypatch.setattr( 710 + health_surface, 711 + "read_segment_backlog", 712 + lambda: _segment_backlog({"20260410": 2}, not_sensed=3), 713 + ) 714 + monkeypatch.setattr( 715 + health_surface, 716 + "load_processing_settings", 717 + lambda: _processing_settings("realtime"), 718 + ) 719 + monkeypatch.setattr( 720 + health_surface, 721 + "evaluate_drain_gate", 722 + lambda settings, now, reading: GateState(open=True, conditions={}), 723 + ) 724 + monkeypatch.setattr(health_surface, "read_last_drained_at", lambda: None) 725 + monkeypatch.setattr("solstone.think.models.no_thinking_engine_chosen", lambda: True) 726 + _stub_display_powersave(monkeypatch) 727 + 728 + backlog = health_surface._build_segment_backlog_health() 729 + 730 + assert backlog.awaiting_analysis_text == health_surface.NO_ENGINE_ANALYSIS_TEXT 731 + assert backlog.drain_state == DRAIN_STATE_NO_ENGINE 696 732 697 733 698 734 def test_segment_backlog_last_drained_at_passes_through(monkeypatch) -> None: