diff --git a/docs/self-evolution.md b/docs/self-evolution.md index abfcc34..7a2c233 100644 --- a/docs/self-evolution.md +++ b/docs/self-evolution.md @@ -9,7 +9,7 @@ Historical verification snapshot: `51be33361422e55e1f2f00c33a0e0f8c56132a91` (the post-#54 `main` revision, captured before this #55 documentation-only update). Snapshot date: 2026-09-04. -Current repository test count at this snapshot: **202 unittest cases**. +Current repository test count at this snapshot: **203 unittest cases**. ## Verified surface diff --git a/main.py b/main.py index a68ddf7..2d04134 100644 --- a/main.py +++ b/main.py @@ -1962,12 +1962,13 @@ def _detect_user_preferences(user_input: str) -> None: detected = {k: v for k, v in _user_preferences.items() if v} if detected: pref_str = "; ".join(f"{k}={v}" for k, v in detected.items()) - # Only store if new or changed - recent = memory_bank.get_recent(3) - already = any(f"PREF: {pref_str}" in r for r in recent) - if not already: - learning_engine.submit("preference", f"PREF: {pref_str}", evidence_id=stable_hash(user_input), - confidence=0.5, metadata={"source": "preference_detection"}) + # Each user turn is an independent observation. Reusing a content hash + # (or suppressing a repeated message) prevents the evidence threshold + # from ever promoting a preference across a process restart. + learning_engine.submit( + "preference", f"PREF: {pref_str}", evidence_id=f"turn-{uuid.uuid4().hex}", + confidence=0.5, metadata={"source": "preference_detection"}, + ) def _build_preference_context() -> str: diff --git a/tests/test_learning_dispatcher.py b/tests/test_learning_dispatcher.py index e64bf16..f247666 100644 --- a/tests/test_learning_dispatcher.py +++ b/tests/test_learning_dispatcher.py @@ -177,6 +177,43 @@ def test_verified_preferences_hydrate_in_fresh_runtime_and_prompt(self): self.assertIn("naming_style=snake_case", completed.stdout) self.assertIn("Known user preferences", completed.stdout) + def test_detected_preference_promotes_and_survives_restart(self): + with tempfile.TemporaryDirectory(prefix="openkyrozen-detected-preference-") as directory: + root = Path(directory) + db_path = root / "state.sqlite3" + env = os.environ.copy() + env.update({ + "HOME": str(root), "KYROZEN_DB_PATH": str(db_path), + "KYROZEN_DISABLE_VECTOR_INDEX": "1", "KYROZEN_WORKSPACE_ROOT": str(root), + "PYTHONPATH": str(Path(__file__).parents[1]), + }) + observe = subprocess.run( + [sys.executable, "-c", ( + "import main; " + f"main.configure_launch_context(project_path={str(root)!r}); " + "[main.dispatch_learning_cycle(surface='cli', trigger='turn', max_features=1, " + "user_input='Please use concise Python and snake_case names.', " + "feature_names=('detect_user_preferences',)) for _ in range(2)]; " + "print(main.learning_engine.status(100)[0]['status'])" + )], + cwd=Path(__file__).parents[1], env=env, capture_output=True, text=True, check=False, + ) + self.assertEqual(observe.returncode, 0, observe.stderr) + self.assertIn("active", observe.stdout) + + fresh = subprocess.run( + [sys.executable, "-c", ( + "import main; " + f"main.configure_launch_context(project_path={str(root)!r}); " + "print(main._build_preference_context())" + )], + cwd=Path(__file__).parents[1], env=env, capture_output=True, text=True, check=False, + ) + self.assertEqual(fresh.returncode, 0, fresh.stderr) + self.assertIn("language=python", fresh.stdout) + self.assertIn("naming_style=snake_case", fresh.stdout) + self.assertIn("verbosity=concise", fresh.stdout) + def test_feature_failure_is_recorded_without_stopping_the_cycle(self): names = main._LEARNING_FEATURE_ORDER[:2] registry = dict(main._LEARNING_FEATURE_REGISTRY)