From a50fda28f96f1a78ee66bdc562c5dfd2a134960b Mon Sep 17 00:00:00 2001 From: buildingjoshbetter Date: Sat, 10 Oct 2026 22:48:05 -0500 Subject: [PATCH 1/2] fix: Report unsupported scheduled preference refresh --- docs/resource-budgets.md | 8 +- tests/test-maintenance-engine-routing-753.py | 4 +- tests/test-preferences-status-746.py | 308 +++++++++++++++++++ tests/test-tier-public-boundaries-795.py | 8 +- tests/test_consolidation_gaps.py | 7 +- truememory/engine.py | 18 +- truememory/maintenance.py | 22 +- truememory/personality.py | 8 +- 8 files changed, 345 insertions(+), 38 deletions(-) create mode 100644 tests/test-preferences-status-746.py diff --git a/docs/resource-budgets.md b/docs/resource-budgets.md index e12aa613..b867ebb4 100644 --- a/docs/resource-budgets.md +++ b/docs/resource-budgets.md @@ -1169,7 +1169,13 @@ references and registry state. Daemon threads do not guarantee completion when the process exits; durable abandoned-attempt recovery remains necessary. Explicit `consolidate()` forces one pass with the same eight result keys plus -the existing nonpersisted preference extraction result. A busy owner produces +`extract_preferences`, which reports `UNAVAILABLE (ScheduledPreferencesUnsupported)`. +Scheduled preference maps have no persistence or refresh implementation; this +stage does no extraction, source read or write. Otherwise complete maintenance +reports `completed_with_limits`. Cancellation, layer failures or deferrals, and +pending caller writes retain precedence. Bulk `ingest()` reports the same +preference limitation. Entity-scoped extraction remains a return-only helper, +and query-time preference retrieval remains available. A busy owner produces nine explicit `BUSY` entries and does not queue a hidden forced pass. A file call without an existing transaction uses a dedicated connection. An existing caller transaction uses the original connection's protected savepoints and diff --git a/tests/test-maintenance-engine-routing-753.py b/tests/test-maintenance-engine-routing-753.py index 4138f079..733b14dd 100644 --- a/tests/test-maintenance-engine-routing-753.py +++ b/tests/test-maintenance-engine-routing-753.py @@ -710,7 +710,9 @@ def run(conn, coordinator, **kwargs): result = self.engine.consolidate() automatic.assert_not_called() self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) - self.assertTrue(all("ERROR" not in value and "UNAVAILABLE" not in value for value in result.values()), result) + self.assertTrue(all("ERROR" not in value and "UNAVAILABLE" not in value + for key, value in result.items() if key != "extract_preferences"), result) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") self.assertIn("vector_generation_unverified", result["cluster_messages"]) with self.assertRaises(sqlite3.ProgrammingError): seen[0].execute("SELECT 1") diff --git a/tests/test-preferences-status-746.py b/tests/test-preferences-status-746.py new file mode 100644 index 00000000..c69717e5 --- /dev/null +++ b/tests/test-preferences-status-746.py @@ -0,0 +1,308 @@ +"""Truthful scheduled-preference status with stdlib and in-memory SQLite only. + +Actual maintenance/serving ownership, layer transactions, status formatting and +ingest admission execute. Synthetic layer builders replace unrelated algorithms; +the personality control loads actual FTS and char-gram retrieval without models. +""" +from __future__ import annotations + +import ast +import logging +import runpy +import sqlite3 +import threading +import time +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +PRIMITIVES = runpy.run_path(str(Path(__file__).with_name("test-maintenance-source-revision-753.py"))) +STORAGE, MAINTENANCE = PRIMITIVES["STORAGE"], PRIMITIVES["MAINTENANCE"] +UNAVAILABLE = "UNAVAILABLE (ScheduledPreferencesUnsupported)" +LAYERS = ( + ("clusters", "cluster_messages"), ("summaries", "build_summaries"), + ("contradictions", "detect_contradictions"), ("structured_facts", "structured_facts"), + ("surprise", "build_surprise_index"), ("episodes", "detect_episodes"), + ("landmarks", "detect_landmarks"), ("dunbar", "dunbar_hierarchy"), +) + + +def source_function(path: str, name: str) -> ast.FunctionDef: + tree = ast.parse((ROOT / path).read_text(encoding="utf-8")) + return next(node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name == name) + + +class TestScheduledPreferences(unittest.TestCase): + def setUp(self) -> None: + self.conn = STORAGE.create_db(":memory:") + self.addCleanup(self.conn.close) + self.conn.execute("CREATE TABLE synthetic_outputs(layer TEXT PRIMARY KEY)") + self.conn.commit() + self.coordinator = MAINTENANCE.MaintenanceCoordinator(None) + self.builds: list[str] = [] + self.imports: list[str] = [] + + def add(self, *, commit: bool = True) -> None: + self.conn.execute( + "INSERT INTO messages(content,sender,timestamp) VALUES " + "('Synthetic coffee routine','synthetic-entity','2026-01-01')" + ) + if commit: + self.conn.commit() + + def specs(self) -> tuple: + def spec(layer: str, key: str) -> object: + def build(conn: sqlite3.Connection) -> None: + self.assertIs(conn, self.conn) + self.assertTrue(conn.in_transaction) + self.builds.append(layer) + conn.execute("INSERT OR REPLACE INTO synthetic_outputs VALUES (?)", (layer,)) + + return MAINTENANCE.LayerSpec( + layer, key, lambda: MAINTENANCE.make_layer_dependency(1, {"synthetic": layer}), + build, lambda conn: conn.execute( + "SELECT count(*) FROM synthetic_outputs WHERE layer=?", (layer,), + ).fetchone()[0], + ) + return tuple(spec(layer, key) for layer, key in LAYERS) + + def forbidden_import(self, name: str) -> object: + self.imports.append(name) + raise AssertionError("The synthetic runner must not import preference/model builders") + + def run_report(self, **kwargs: object) -> object: + with patch.object(MAINTENANCE, "engine_layer_specs", lambda *_args, **_kwargs: self.specs()), \ + patch.object(MAINTENANCE, "importlib", types.SimpleNamespace(import_module=self.forbidden_import)): + return MAINTENANCE.run_engine_maintenance( + self.conn, self.coordinator, prepare_extensions=False, **kwargs, + ) + + def result(self, outcome: str = "success", **kwargs: object) -> object: + return MAINTENANCE.LayerResult( + "summaries", "build_summaries", outcome, 1, 0.0, + kwargs.get("error_category"), kwargs.get("attempted", True), + kwargs.get("coverage", "complete"), kwargs.get("pending_caller_commit", False), + ) + + def test_forced_empty_and_populated_runs_keep_eight_layers_and_report_limit(self) -> None: + self.assertEqual(MAINTENANCE.SCHEDULED_PREFERENCES_UNAVAILABLE, UNAVAILABLE) + for populated in (False, True): + with self.subTest(populated=populated): + if populated: + self.add() + self.builds.clear() + report = self.run_report(force=True) + self.assertEqual(self.builds, [layer for layer, _ in LAYERS]) + self.assertTrue(all(result.outcome == "success" for result in report.results)) + self.assertEqual(report.preferences, UNAVAILABLE) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("completed_with_limits", None)) + formatted = MAINTENANCE.format_maintenance_report(report) + self.assertEqual(set(formatted), {key for _, key in LAYERS} | {"extract_preferences"}) + self.assertEqual(formatted["extract_preferences"], UNAVAILABLE) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.imports, []) + + def test_eligible_automatic_attempt_and_current_no_attempt_differ(self) -> None: + self.add() + attempted = self.run_report(threshold=1) + self.assertTrue(all(result.attempted for result in attempted.results)) + self.assertEqual(attempted.preferences, UNAVAILABLE) + self.builds.clear() + current = self.run_report(threshold=1) + self.assertEqual(self.builds, []) + self.assertTrue(all(not result.attempted for result in current.results)) + self.assertEqual(current.preferences, "SKIPPED (no maintenance attempt)") + self.assertEqual(MAINTENANCE.maintenance_report_status(current), ("success", None)) + self.assertEqual(self.imports, []) + + def test_borrowed_writes_still_rollback_and_keep_pending_precedence(self) -> None: + self.add(commit=False) + report = self.run_report(force=True, allow_caller_transaction=True) + self.assertTrue(self.conn.in_transaction) + self.assertTrue(all(result.pending_caller_commit for result in report.results)) + self.assertEqual(report.preferences, UNAVAILABLE) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("pending", None)) + self.assertEqual(self.conn.execute("SELECT count(*) FROM synthetic_outputs").fetchone()[0], 8) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM synthetic_outputs").fetchone()[0], 0) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + + def test_captured_cancellation_survives_cleared_event(self) -> None: + for include_style in (False, True): + with self.subTest(include_style=include_style): + cancel = threading.Event() + cancel.set() + report = self.run_report(force=True, cancel=cancel, include_style=include_style) + cancel.clear() + self.assertEqual(report.preferences, "CANCELLED") + self.assertEqual(MAINTENANCE.maintenance_report_status(report, cancel), ("cancelled", "Cancelled")) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("cancelled", "Cancelled")) + self.assertEqual(self.builds, []) + self.assertEqual(self.imports, []) + + def test_real_layer_and_style_outcomes_outrank_known_limitation(self) -> None: + for outcome in ("failed", "unavailable", "deferred", "abandoned"): + for style in (False, True): + with self.subTest(outcome=outcome, style=style): + result = self.result(outcome, error_category="SyntheticFailure") + report = MAINTENANCE.MaintenanceReport( + () if style else (result,), UNAVAILABLE, result if style else None, + ) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), (outcome, "SyntheticFailure")) + cancel = threading.Event() + cancel.set() + self.assertEqual(MAINTENANCE.maintenance_report_status(report, cancel), ("cancelled", "Cancelled")) + + def test_pending_layer_and_style_results_outrank_known_limitation(self) -> None: + for result in (self.result(pending_caller_commit=True), self.result("stale"), self.result("untrusted")): + for style in (False, True): + with self.subTest(result=result, style=style): + report = MAINTENANCE.MaintenanceReport( + () if style else (result,), UNAVAILABLE, result if style else None, + ) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("pending", None)) + + def test_only_exact_unsupported_token_is_a_completion_limit(self) -> None: + for status in ("ERROR (SyntheticFailure)", "UNAVAILABLE (DependencyMissing)", + "UNAVAILABLE (ScheduledPreferencesUnsupportedExtra)", UNAVAILABLE + " extra", + UNAVAILABLE + " (pending caller commit)"): + with self.subTest(status=status): + report = MAINTENANCE.MaintenanceReport((self.result(),), status) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("failed", "PreferencesUnavailable")) + report = MAINTENANCE.MaintenanceReport((self.result(),), UNAVAILABLE) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("completed_with_limits", None)) + + def test_style_only_and_busy_contracts_remain_distinct(self) -> None: + style = self.result()._replace(layer="style_vectors", result_key="style_vectors") + with patch.object(MAINTENANCE, "run_routed_style", return_value=style) as builder: + report = self.run_report(force=True, include_layers=False, include_style=True) + self.assertIs(report.style_result, style) + builder.assert_called_once() + self.assertIs(builder.call_args.args[0], self.conn) + self.assertEqual(report.preferences, "SKIPPED (style-only)") + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("success", None)) + self.assertEqual(self.builds, []) + busy = MAINTENANCE.maintenance_busy_result() + self.assertEqual(set(busy), {key for _, key in LAYERS} | {"extract_preferences"}) + self.assertTrue(all(value == "BUSY (maintenance owner active)" for value in busy.values())) + + def test_preference_phase_uses_no_connection_or_helper(self) -> None: + function = source_function("truememory/maintenance.py", "run_engine_maintenance") + body = function.body[-1].body + start = next(index for index, node in enumerate(body) if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "preferences" for target in node.targets)) + wrapper = ast.parse("def phase(conn, results, force, cancel, style_result):\n pass\n").body[0] + wrapper.body = body[start:] + forbidden: list[str] = [] + + class NoConnectionWork: + def __getattribute__(self, name: str) -> object: + forbidden.append(name) + raise AssertionError("Unsupported preference phase cannot use its connection") + + namespace = dict(MAINTENANCE.__dict__) + namespace["importlib"] = types.SimpleNamespace(import_module=self.forbidden_import) + exec(compile(ast.fix_missing_locations(ast.Module(body=[wrapper], type_ignores=[])), + "synthetic-preference-phase", "exec"), namespace) + for force, attempted in ((True, False), (False, True), (False, False)): + with self.subTest(force=force, attempted=attempted): + result = namespace["phase"](NoConnectionWork(), (self.result(attempted=attempted),), force, None, None) + self.assertEqual(result.preferences, UNAVAILABLE if force or attempted else "SKIPPED (no maintenance attempt)") + self.assertEqual(forbidden, []) + self.assertEqual(self.imports, []) + + def test_actual_ingest_reports_present_and_absent_personality_truthfully(self) -> None: + function = source_function("truememory/engine.py", "ingest") + calls: list[str] = [] + + def create(path: Path) -> sqlite3.Connection: + self.assertEqual(str(path), ":memory:") + conn = STORAGE.create_db(":memory:") + self.addCleanup(conn.close) + return conn + + def forbidden_preferences(*_args: object, **_kwargs: object) -> None: + calls.append("preferences") + raise AssertionError("Ingest must not call no-entity preference extraction") + + def load(conn: sqlite3.Connection, _path: Path) -> int: + conn.execute("INSERT INTO messages(content,sender,timestamp) VALUES ('Synthetic coffee','synthetic','2026-01-01')") + conn.commit() + return 1 + + for available in (True, False): + with self.subTest(personality_available=available): + namespace = { + "__builtins__": STORAGE.__dict__["__builtins__"], "Path": Path, "time": time, + "os": types.SimpleNamespace(environ={}), "logger": logging.getLogger("synthetic-preference-ingest"), + "create_db": create, "load_messages_from_file": load, + "build_entity_profiles": lambda _conn: calls.append("profiles"), + "build_dunbar_hierarchy": lambda _conn, **_kwargs: {}, + "extract_preferences": forbidden_preferences, + } + namespace.update({node.id: False for node in ast.walk(function) + if isinstance(node, ast.Name) and node.id.startswith("_HAS_")}) + namespace["_HAS_PERSONALITY"] = available + exec(compile(ast.fix_missing_locations(ast.Module(body=[function], type_ignores=[])), + "synthetic-actual-ingest", "exec"), namespace) + flags = {name: False for name in ("vectors", "hybrid", "temporal", "salience", "personality", + "style_vec", "consolidation", "predictive", "reranker", "hyde", "clustering")} + engine = types.SimpleNamespace(db_path=Path(":memory:"), conn=None, stats={}, + _get_maintenance_coordinator=lambda: self.coordinator, + **{"_has_" + key: value for key, value in flags.items()}) + calls.clear() + result = namespace["ingest"](engine, Path("synthetic-unused-input.json")) + expected = UNAVAILABLE if available else "SKIPPED (personality module not available)" + self.assertEqual(result["extract_preferences"], expected) + self.assertEqual(calls, ["profiles"] if available else []) + self.assertEqual(engine.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertTrue(engine.ready) + + def test_entity_helper_and_actual_query_time_retrieval_remain_available(self) -> None: + modules = MAINTENANCE._fixture_modules.copy() + builtins_map = STORAGE.__dict__["__builtins__"] + base_import = builtins_map["__import__"] + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name.startswith("truememory.") and name.removeprefix("truememory.") in modules: + return modules[name.removeprefix("truememory.")] + return base_import(name, globals, locals, fromlist, level) + + for name in ("fts_search", "personality_style_vec", "personality"): + module = types.ModuleType("synthetic_preference_" + name) + module.__dict__["__builtins__"] = dict(builtins_map, __import__=safe_import) + path = ROOT / "truememory" / (name + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + modules[name] = module + api = modules["personality"] + self.conn.executemany("INSERT INTO messages(content,sender,timestamp,directive) VALUES (?,?,?,?)", [ + ("Synthetic Cedar likes coffee every morning", "Cedar", "2026-01-01", 0), + ("Synthetic Birch likes pizza for dinner", "Birch", "2026-01-02", 0), + ("Synthetic Cedar directive says sushi", "Cedar", "2026-01-03", 1), + ]) + self.conn.commit() + statements: list[str] = [] + changes = self.conn.total_changes + self.conn.set_trace_callback(statements.append) + self.assertEqual(api.extract_preferences(self.conn), {}) + self.assertEqual(statements, []) + cedar = api.extract_preferences(self.conn, "Cedar") + birch = api.extract_preferences(self.conn, "Birch") + self.assertTrue(any("coffee" in item["text"] for item in cedar["food"])) + self.assertTrue(any("pizza" in item["text"] for item in birch["food"])) + self.assertFalse(any("sushi" in item["text"] for item in cedar["food"])) + results = api.search_personality(self.conn, "What food does Cedar like?", limit=10) + self.assertTrue(any("coffee" in item["content"] for item in results)) + self.assertFalse(any("sushi" in item["content"] for item in results)) + self.assertTrue(any("MATCH" in statement.upper() for statement in statements)) + self.assertEqual(self.conn.total_changes, changes) + self.assertFalse(self.conn.in_transaction) + self.conn.set_trace_callback(None) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-public-boundaries-795.py b/tests/test-tier-public-boundaries-795.py index bb6040d3..56e26e9f 100644 --- a/tests/test-tier-public-boundaries-795.py +++ b/tests/test-tier-public-boundaries-795.py @@ -42,7 +42,10 @@ def definitions(path: str, names: set[str], namespace: dict, *, methods: bool = cls = next(node for node in tree.body if isinstance(node, ast.ClassDef)) nodes = [node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name in names] else: - nodes = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names] + nodes = [node for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names + or isinstance(node, ast.Assign) and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) and node.targets[0].id in names] future = ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0) module = types.ModuleType("synthetic_public_" + path.replace("/", "_").replace(".", "_")) module.__dict__.update(namespace) @@ -717,7 +720,8 @@ def test_policy_loaded_runtime_cannot_be_inherited_by_unselected_database(self) def maintenance_runner(self, *, owner=None, run_layers=None): owner = owner or (lambda path: contextlib.nullcontext()) module = definitions("maintenance.py", { - "LayerResult", "MaintenanceReport", "_maintenance_embedding_scope", "_maintenance_completion", "run_engine_maintenance", + "LayerResult", "MaintenanceReport", "SCHEDULED_PREFERENCES_UNAVAILABLE", + "_maintenance_embedding_scope", "_maintenance_completion", "run_engine_maintenance", }, self.namespace( NamedTuple=__import__("typing").NamedTuple, contextmanager=contextlib.contextmanager, maintenance_owner=owner, connection_database_path=lambda conn: None, diff --git a/tests/test_consolidation_gaps.py b/tests/test_consolidation_gaps.py index 5344444d..12ce1cc1 100644 --- a/tests/test_consolidation_gaps.py +++ b/tests/test_consolidation_gaps.py @@ -16,14 +16,13 @@ def test_clustering_count_matches_real_published_centroids(self): self.assertIn("3 clusters", result["cluster_messages"]) self.assertEqual(self.conn.execute("SELECT count(*) FROM cluster_centroids").fetchone()[0], 3) - def test_preferences_runs_on_the_worker_connection(self): + def test_scheduled_preferences_report_unavailable_without_extraction(self): function = self.modules["truememory.personality"].extract_preferences with patch.object(self.modules["truememory.personality"], "extract_preferences", wraps=function) as preference: result = self.engine.consolidate() self.assertIn("extract_preferences", result) - self.assertNotIn("ERROR", result["extract_preferences"]) - preference.assert_called_once() - self.assertIsNot(preference.call_args.args[0], self.conn) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") + preference.assert_not_called() def test_missing_vector_index_reports_unavailable_without_skipping_siblings(self): self.conn.execute("DROP TABLE vec_messages_edge") diff --git a/truememory/engine.py b/truememory/engine.py index 08547e2e..0463200a 100644 --- a/truememory/engine.py +++ b/truememory/engine.py @@ -188,7 +188,6 @@ def get_vectors_load_error() -> str | None: try: from truememory.personality import ( build_entity_profiles, - extract_preferences, search_personality, PERSONALITY_ASPECTS, build_dunbar_hierarchy, @@ -1842,7 +1841,7 @@ def ingest(self, data_path: str | Path) -> dict: 2. Load messages from JSON file 3. Build vector embeddings (Model2Vec -> sqlite-vec) 4. Build entity profiles (L0) - 5. Extract preferences (L0) + 5. Report unavailable scheduled preference refresh (L0) 6. Build summaries (L5) 7. Detect contradictions (L5) 8. Build surprise index (predictive coding) @@ -1852,7 +1851,7 @@ def ingest(self, data_path: str | Path) -> dict: engine still works with FTS5 only. Returns: - Dict mapping step names to timing strings (or error messages). + Dict mapping step names to timing strings or explicit status messages. """ from truememory.tier_switch.runtime import destructive_ingest_operation with destructive_ingest_operation(self): @@ -1977,17 +1976,10 @@ def ingest(self, data_path: str | Path) -> dict: else: stats["build_style_vectors"] = "SKIPPED (personality_style_vec module not available)" - # ── 5. Extract preferences (L0) ─────────────────────────────────── + # Scheduled preference maps have no persistence/refresh implementation. if _HAS_PERSONALITY: - try: - t0 = time.time() - extract_preferences(self.conn) - stats["extract_preferences"] = f"{time.time() - t0:.3f}s" - except Exception as exc: - from truememory.tier_switch.runtime import raise_if_serving_rejection - raise_if_serving_rejection(exc) - stats["extract_preferences"] = f"ERROR: {exc}" - logger.debug("extract_preferences failed", exc_info=True) + from truememory.maintenance import SCHEDULED_PREFERENCES_UNAVAILABLE + stats["extract_preferences"] = SCHEDULED_PREFERENCES_UNAVAILABLE else: stats["extract_preferences"] = "SKIPPED (personality module not available)" diff --git a/truememory/maintenance.py b/truememory/maintenance.py index dcc353c4..425da96d 100644 --- a/truememory/maintenance.py +++ b/truememory/maintenance.py @@ -2427,6 +2427,9 @@ def dunbar_dependency() -> LayerDependency: )) +SCHEDULED_PREFERENCES_UNAVAILABLE = "UNAVAILABLE (ScheduledPreferencesUnsupported)" + + class MaintenanceReport(NamedTuple): results: tuple[LayerResult, ...] preferences: str @@ -2657,33 +2660,24 @@ def run_engine_maintenance( if cancel is not None and cancel.is_set(): preferences = "CANCELLED" elif force or any(result.attempted for result in results): - started = time.monotonic() - try: - importlib.import_module("truememory.personality").extract_preferences(conn) - except (ImportError, AttributeError): - preferences = "UNAVAILABLE (DependencyMissing)" - except Exception as error: - preferences = "ERROR (" + type(error).__name__[:64] + ")" - else: - preferences = f"{time.monotonic() - started:.3f}s" - if conn.in_transaction and allow_caller_transaction: - preferences += " (pending caller commit)" + preferences = SCHEDULED_PREFERENCES_UNAVAILABLE return MaintenanceReport(results, preferences, style_result) def maintenance_report_status(report: MaintenanceReport, cancel: threading.Event | None = None) -> tuple[str, str | None]: - if cancel is not None and cancel.is_set(): + if report.preferences == "CANCELLED" or cancel is not None and cancel.is_set(): return "cancelled", "Cancelled" results = report.results + ((report.style_result,) if report.style_result is not None else ()) for outcome in ("failed", "unavailable", "deferred", "abandoned"): for result in results: if result.outcome == outcome: return outcome, result.error_category - if report.preferences.startswith(("ERROR", "UNAVAILABLE")): + preferences_unsupported = report.preferences == SCHEDULED_PREFERENCES_UNAVAILABLE + if report.preferences.startswith(("ERROR", "UNAVAILABLE")) and not preferences_unsupported: return "failed", "PreferencesUnavailable" if any(result.pending_caller_commit or result.outcome not in {"success", "success_empty", "current"} for result in results): return "pending", None - if any(result.coverage != "complete" for result in results): + if preferences_unsupported or any(result.coverage != "complete" for result in results): return "completed_with_limits", None return "success", None diff --git a/truememory/personality.py b/truememory/personality.py index f1dc8097..35250b93 100644 --- a/truememory/personality.py +++ b/truememory/personality.py @@ -8,8 +8,8 @@ returns nothing useful. This module pre-computes entity profiles (communication style, topics, -relationships, traits, preferences) so that personality questions can be -answered from structured data rather than raw message search alone. +relationships and traits). Preference questions use query-time retrieval; +scheduled preference-map persistence and refresh are not implemented. Personality categories handled: - General character (``"What kind of person is X?"``) @@ -597,7 +597,9 @@ def extract_preferences(conn: sqlite3.Connection, Returns: Dict with keys ``food``, ``activities``, ``people``, ``routines``, ``fears``, ``values``. Each value is a list of extracted snippets - or structured data. + or structured data. With no entity, returns an empty dict without + reading messages. Results are returned only, never persisted; this + helper is not a scheduled preference refresh. """ if entity is None: return {} From a5ce23b3409a382ffba4d3019258b5db22c99b14 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter Date: Sat, 10 Oct 2026 23:09:20 -0500 Subject: [PATCH 2/2] test: Distinguish unsupported preferences from pending writes --- tests/test-l0-style-routing-747.py | 13 ++++++++++--- tests/test-maintenance-engine-routing-753.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test-l0-style-routing-747.py b/tests/test-l0-style-routing-747.py index 9e0c6914..1250ff45 100644 --- a/tests/test-l0-style-routing-747.py +++ b/tests/test-l0-style-routing-747.py @@ -1039,13 +1039,20 @@ def test_manual_pending_result_and_health_do_not_clear_prior_completed_failure(s token = coordinator._begin_observation() coordinator._finish_observation(token, MAINTENANCE.MaintenanceReport((), "SKIPPED"), "failed", "PriorFailure") conn.execute("INSERT INTO sentinel VALUES ('caller')") + personality = types.SimpleNamespace(extract_preferences=lambda _conn: None) importer = types.SimpleNamespace(import_module=lambda name: STYLE if name == "truememory.personality_style_vec" - else types.SimpleNamespace(extract_preferences=lambda _conn: None)) + else personality) with patch.object(MAINTENANCE, "engine_layer_specs", public_specs), patch.object(MAINTENANCE, "importlib", importer), \ - patch.object(MAINTENANCE, "_installed_dependency", return_value=None): + patch.object(MAINTENANCE, "_installed_dependency", return_value=None), \ + patch.object(personality, "extract_preferences", + side_effect=AssertionError("unsupported preference work")) as preferences: result = engine.consolidate() + preferences.assert_not_called() self.assertEqual(len(result), 9) - self.assertTrue(all("pending caller commit" in value for value in result.values())) + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + self.assertTrue(all("pending caller commit" in result[key] + for key in MAINTENANCE._MAINTENANCE_RESULT_KEYS if key != "extract_preferences"), result) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") self.assertEqual(coordinator.status, ("failed", "PriorFailure")) self.assertEqual(engine.get_style_health()["last_error"], "PriorFailure") conn.rollback() diff --git a/tests/test-maintenance-engine-routing-753.py b/tests/test-maintenance-engine-routing-753.py index 733b14dd..2d557330 100644 --- a/tests/test-maintenance-engine-routing-753.py +++ b/tests/test-maintenance-engine-routing-753.py @@ -734,9 +734,15 @@ def test_24_then_25_committed_adds_across_engines_use_durable_counts(self) -> No def test_borrowed_manual_preserves_caller_writes_and_rollback(self) -> None: self.add(1, commit=False) - with patch.object(self.engine_module, "create_db", side_effect=AssertionError("new handle")): + with patch.object(self.engine_module, "create_db", side_effect=AssertionError("new handle")), \ + patch.object(self.modules["truememory.personality"], "extract_preferences", + side_effect=AssertionError("unsupported preference work")) as preferences: result = self.engine.consolidate() - self.assertTrue(all("pending caller commit" in value for value in result.values()), result) + preferences.assert_not_called() + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + self.assertTrue(all("pending caller commit" in result[key] + for key in MAINTENANCE._MAINTENANCE_RESULT_KEYS if key != "extract_preferences"), result) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") self.assertTrue(self.conn.in_transaction) self.conn.rollback() self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0)