fix: deprecate ~/.tower/timmy-state.txt — consolidate on presence.json (#388)
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
This commit was merged in pull request #388.
This commit is contained in:
@@ -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)."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user