From 858264be0ddf2494fa16f7c0f1ae21a880501ab9 Mon Sep 17 00:00:00 2001 From: Kimi Agent Date: Thu, 19 Mar 2026 01:18:52 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20deprecate=20~/.tower/timmy-state.txt=20?= =?UTF-8?q?=E2=80=94=20consolidate=20on=20presence.json=20(#388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kimi Agent Co-committed-by: Kimi Agent --- src/timmy/cognitive_state.py | 54 +++++--------------------- tests/timmy/test_cognitive_state.py | 60 ++++++++--------------------- 2 files changed, 24 insertions(+), 90 deletions(-) diff --git a/src/timmy/cognitive_state.py b/src/timmy/cognitive_state.py index e513ac0a..2ec2450d 100644 --- a/src/timmy/cognitive_state.py +++ b/src/timmy/cognitive_state.py @@ -4,24 +4,14 @@ Tracks Timmy's internal cognitive signals — focus, engagement, mood, and active commitments — so external systems (Matrix avatar, dashboard) can render observable behaviour. -State persists to ``~/.tower/timmy-state.txt`` alongside the existing -loop coordination fields. - -Schema (YAML-ish in the state file):: - - FOCUS_TOPIC: three-phase loop architecture - ENGAGEMENT: deep - MOOD: curious - CONVERSATION_DEPTH: 42 - LAST_INITIATIVE: proposed Unsplash API exploration - ACTIVE_COMMITMENTS: draft skeleton ticket +State is published via ``workshop_state.py`` → ``presence.json`` and the +WebSocket relay. The old ``~/.tower/timmy-state.txt`` file has been +deprecated (see #384). """ import json import logging from dataclasses import asdict, dataclass, field -from datetime import UTC, datetime -from pathlib import Path from timmy.confidence import estimate_confidence @@ -34,8 +24,6 @@ logger = logging.getLogger(__name__) ENGAGEMENT_LEVELS = ("idle", "surface", "deep") MOOD_VALUES = ("curious", "settled", "hesitant", "energized") -STATE_FILE = Path.home() / ".tower" / "timmy-state.txt" - @dataclass class CognitiveState: @@ -63,21 +51,6 @@ class CognitiveState: d.pop("_confidence_count", None) return d - def to_state_lines(self) -> list[str]: - """Format for ``~/.tower/timmy-state.txt``.""" - lines = [ - f"LAST_UPDATED: {datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}", - f"FOCUS_TOPIC: {self.focus_topic or 'none'}", - f"ENGAGEMENT: {self.engagement}", - f"MOOD: {self.mood}", - f"CONVERSATION_DEPTH: {self.conversation_depth}", - ] - if self.last_initiative: - lines.append(f"LAST_INITIATIVE: {self.last_initiative}") - if self.active_commitments: - lines.append(f"ACTIVE_COMMITMENTS: {'; '.join(self.active_commitments)}") - return lines - # --------------------------------------------------------------------------- # Cognitive signal extraction @@ -183,11 +156,14 @@ def _extract_commitments(response: str) -> list[str]: class CognitiveTracker: - """Maintains and persists Timmy's cognitive state.""" + """Maintains Timmy's cognitive state. - def __init__(self, state_file: Path | None = None) -> None: + State is consumed via ``to_json()`` / ``get_state()`` and published + externally by ``workshop_state.py`` → ``presence.json``. + """ + + def __init__(self) -> None: self.state = CognitiveState() - self._state_file = state_file or STATE_FILE def update(self, user_message: str, response: str) -> CognitiveState: """Update cognitive state from a chat exchange. @@ -217,9 +193,6 @@ class CognitiveTracker: seen.add(c) self.state.active_commitments = self.state.active_commitments[-5:] - # Persist to disk (best-effort) - self._write_state_file() - return self.state def get_state(self) -> CognitiveState: @@ -229,15 +202,6 @@ class CognitiveTracker: def reset(self) -> None: """Reset to idle state (e.g. on session reset).""" self.state = CognitiveState() - self._write_state_file() - - def _write_state_file(self) -> None: - """Persist state to ``~/.tower/timmy-state.txt``.""" - try: - self._state_file.parent.mkdir(parents=True, exist_ok=True) - self._state_file.write_text("\n".join(self.state.to_state_lines()) + "\n") - except OSError as exc: - logger.warning("Failed to write cognitive state: %s", exc) def to_json(self) -> str: """Serialise current state as JSON (for API / WebSocket consumers).""" diff --git a/tests/timmy/test_cognitive_state.py b/tests/timmy/test_cognitive_state.py index 409ecc11..2b1596a8 100644 --- a/tests/timmy/test_cognitive_state.py +++ b/tests/timmy/test_cognitive_state.py @@ -31,7 +31,7 @@ class TestCognitiveState: assert "_confidence_sum" not in d assert "_confidence_count" not in d - def test_to_state_lines_format(self): + def test_to_dict_includes_public_fields(self): state = CognitiveState( focus_topic="loop architecture", engagement="deep", @@ -40,26 +40,11 @@ class TestCognitiveState: last_initiative="proposed refactor", active_commitments=["draft ticket", "review PR"], ) - lines = state.to_state_lines() - text = "\n".join(lines) - assert "FOCUS_TOPIC: loop architecture" in text - assert "ENGAGEMENT: deep" in text - assert "MOOD: curious" in text - assert "CONVERSATION_DEPTH: 42" in text - assert "LAST_INITIATIVE: proposed refactor" in text - assert "ACTIVE_COMMITMENTS: draft ticket; review PR" in text - - def test_to_state_lines_none_topic(self): - state = CognitiveState() - lines = state.to_state_lines() - text = "\n".join(lines) - assert "FOCUS_TOPIC: none" in text - - def test_to_state_lines_no_commitments(self): - state = CognitiveState() - lines = state.to_state_lines() - text = "\n".join(lines) - assert "ACTIVE_COMMITMENTS" not in text + d = state.to_dict() + assert d["focus_topic"] == "loop architecture" + assert d["engagement"] == "deep" + assert d["mood"] == "curious" + assert d["conversation_depth"] == 42 class TestInferEngagement: @@ -144,43 +129,34 @@ class TestExtractCommitments: class TestCognitiveTracker: - """Test the CognitiveTracker singleton behaviour.""" + """Test the CognitiveTracker behaviour.""" - def test_update_increments_depth(self, tmp_path): - tracker = CognitiveTracker(state_file=tmp_path / "state.txt") + def test_update_increments_depth(self): + tracker = CognitiveTracker() tracker.update("hello", "Hi there, how can I help?") assert tracker.get_state().conversation_depth == 1 tracker.update("thanks", "You're welcome!") assert tracker.get_state().conversation_depth == 2 - def test_update_sets_focus_topic(self, tmp_path): - tracker = CognitiveTracker(state_file=tmp_path / "state.txt") + def test_update_sets_focus_topic(self): + tracker = CognitiveTracker() tracker.update( "Python decorators", "Decorators are syntactic sugar for wrapping functions." ) assert tracker.get_state().focus_topic == "Python decorators" - def test_update_persists_to_file(self, tmp_path): - state_file = tmp_path / "state.txt" - tracker = CognitiveTracker(state_file=state_file) - tracker.update("debug the loop", "Let me investigate the issue.") - assert state_file.exists() - content = state_file.read_text() - assert "ENGAGEMENT:" in content - assert "MOOD:" in content - - def test_reset_clears_state(self, tmp_path): - tracker = CognitiveTracker(state_file=tmp_path / "state.txt") + def test_reset_clears_state(self): + tracker = CognitiveTracker() tracker.update("hello", "world") tracker.reset() state = tracker.get_state() assert state.conversation_depth == 0 assert state.focus_topic is None - def test_to_json(self, tmp_path): + def test_to_json(self): import json - tracker = CognitiveTracker(state_file=tmp_path / "state.txt") + tracker = CognitiveTracker() tracker.update("test", "response") data = json.loads(tracker.to_json()) assert "focus_topic" in data @@ -190,9 +166,3 @@ class TestCognitiveTracker: def test_engagement_values_are_valid(self): for level in ENGAGEMENT_LEVELS: assert isinstance(level, str) - - def test_creates_parent_directory(self, tmp_path): - state_file = tmp_path / "nested" / "dir" / "state.txt" - tracker = CognitiveTracker(state_file=state_file) - tracker.update("test", "response") - assert state_file.exists()