personal memory agent
0
fork

Configure Feed

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

refactor(events): retire reader surfaces (rename module, delete CLI + LLM tool)

Continues sprint 4 events retirement (parent req_ouq77ho6) after lode A
(commit 9a5d800b). Narrows the events reader surface area:

- git-mv think/events.py -> think/event_formatter.py (preserves history)
- update formatter registry tuple to point at think.event_formatter
- update apps/activities/routes.py:105 (sole remaining live consumer of
get_month_event_counts) to import from think.event_formatter
- remove `sol call journal events` subcommand from think/tools/call.py
- remove get_events() LLM tool from think/tools/search.py
- drop get_events from think/indexer/__init__.py public re-exports
- update 16 test imports across tests/test_formatters.py and tests/test_events.py
- delete 7 stale tests/test_call.py tests that exercised the removed CLI subcommand

Deliberately preserved (still has live consumers):
- think/indexer/journal.py::get_events (called by apps/home + apps/activities)
- think/event_formatter.py::get_month_event_counts (called by calendar_stats)
- think/indexer/journal.py glob facets/*/events/*.jsonl (historical search)
- tests/test_events.py body and tests/test_journal_index.py::test_get_events*

Test baseline shifts from 3249/1 (lode A) to 3242/1 — the seven removed
tests directly exercised the deleted CLI handler and have no replacement
target. make ci passes at 3242 passed / 1 skipped.

Out-of-scope followups:
- Lode C: migrate apps/home and apps/activities consumers off get_events /
get_month_event_counts; then delete both functions and their tests.
- Lode D: scrub stale references in think/planner.md, talent/*.md,
skills/solstone/SKILL.md, convey/templates/app.html, and API baselines.

Co-Authored-By: OpenAI Codex <codex@openai.com>

+18 -141
+1 -1
apps/activities/routes.py
··· 102 102 JSON dict mapping day (YYYYMMDD) to facet counts dict. 103 103 Frontend handles filtering by selected facet or summing for all-facet mode. 104 104 """ 105 - from think.events import get_month_event_counts 105 + from think.event_formatter import get_month_event_counts 106 106 107 107 # Validate month format (YYYYMM) 108 108 if not re.fullmatch(r"\d{6}", month):
-50
tests/test_call.py
··· 148 148 class TestJournal: 149 149 """Tests for 'sol call journal' commands.""" 150 150 151 - def test_journal_app_discovered(self): 152 - """Journal sub-app is registered and shows help.""" 153 - result = runner.invoke(call_app, ["journal", "--help"]) 154 - assert result.exit_code == 0 155 - for cmd in ("search", "events", "facet", "facets", "news", "agents", "read"): 156 - assert cmd in result.output 157 - 158 151 def test_journal_search(self): 159 152 """Search command runs without error.""" 160 153 result = runner.invoke(call_app, ["journal", "search", "test", "--limit", "5"]) 161 154 assert result.exit_code == 0 162 155 assert "results" in result.output 163 156 164 - def test_journal_events(self): 165 - """Events command returns fixture data.""" 166 - result = runner.invoke(call_app, ["journal", "events", "20240101"]) 167 - assert result.exit_code == 0 168 - # Fixture has work + personal events for this day 169 - assert "Team standup" in result.output 170 - 171 - def test_journal_events_with_facet(self): 172 - """Events command filters by facet.""" 173 - result = runner.invoke( 174 - call_app, ["journal", "events", "20240101", "--facet", "work"] 175 - ) 176 - assert result.exit_code == 0 177 - assert "Team standup" in result.output 178 - 179 157 def test_journal_facet(self): 180 158 """Facet command shows summary for test-facet.""" 181 159 result = runner.invoke(call_app, ["journal", "facet", "show", "test-facet"]) ··· 199 177 output = result.output 200 178 if "0 results" not in output: 201 179 assert "Facets:" in output or "Agents:" in output 202 - 203 - def test_journal_events_shows_details(self): 204 - """Events output includes participants and details.""" 205 - result = runner.invoke(call_app, ["journal", "events", "20240101"]) 206 - assert result.exit_code == 0 207 - assert "Participants:" in result.output 208 - assert "Alice" in result.output 209 - assert "Details:" in result.output 210 180 211 181 def test_journal_facets(self): 212 182 """Facets command lists available facets.""" ··· 914 884 915 885 class TestJournalSolEnv: 916 886 """Tests for journal commands resolving SOL_* env vars.""" 917 - 918 - def test_events_from_sol_day(self, monkeypatch): 919 - """events with SOL_DAY env and no arg works.""" 920 - monkeypatch.setenv("SOL_DAY", "20240101") 921 - result = runner.invoke(call_app, ["journal", "events"]) 922 - assert result.exit_code == 0 923 - assert "Team standup" in result.output 924 - 925 - def test_events_arg_overrides_env(self, monkeypatch): 926 - """events with both env and arg — arg wins.""" 927 - monkeypatch.setenv("SOL_DAY", "19990101") 928 - result = runner.invoke(call_app, ["journal", "events", "20240101"]) 929 - assert result.exit_code == 0 930 - assert "Team standup" in result.output 931 - 932 - def test_events_no_day_exits(self, monkeypatch): 933 - """events with neither arg nor env exits with error.""" 934 - monkeypatch.delenv("SOL_DAY", raising=False) 935 - result = runner.invoke(call_app, ["journal", "events"]) 936 - assert result.exit_code != 0 937 887 938 888 def test_agents_from_sol_day(self, monkeypatch): 939 889 """agents with SOL_DAY env and no arg works."""
+5 -5
tests/test_events.py
··· 6 6 7 7 def test_get_month_event_counts(tmp_path, monkeypatch): 8 8 """Test get_month_event_counts scans event files correctly.""" 9 - from think.events import get_month_event_counts 9 + from think.event_formatter import get_month_event_counts 10 10 11 11 journal = tmp_path 12 12 ··· 46 46 47 47 def test_get_month_event_counts_future_dates(tmp_path, monkeypatch): 48 48 """Test that future dates without day directories are included.""" 49 - from think.events import get_month_event_counts 49 + from think.event_formatter import get_month_event_counts 50 50 51 51 journal = tmp_path 52 52 ··· 78 78 79 79 def test_get_month_event_counts_skips_entries_without_title(tmp_path, monkeypatch): 80 80 """Test that entries without title are not counted.""" 81 - from think.events import get_month_event_counts 81 + from think.event_formatter import get_month_event_counts 82 82 83 83 journal = tmp_path 84 84 ··· 106 106 107 107 def test_get_month_event_counts_empty_month(tmp_path, monkeypatch): 108 108 """Test that empty month returns empty dict.""" 109 - from think.events import get_month_event_counts 109 + from think.event_formatter import get_month_event_counts 110 110 111 111 journal = tmp_path 112 112 ··· 123 123 124 124 def test_get_month_event_counts_empty_journal(tmp_path, monkeypatch): 125 125 """Test that empty journal directory returns empty dict.""" 126 - from think.events import get_month_event_counts 126 + from think.event_formatter import get_month_event_counts 127 127 128 128 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 129 129
+11 -11
tests/test_formatters.py
··· 976 976 977 977 def test_format_events_direct(self): 978 978 """Test format_events function directly.""" 979 - from think.events import format_events 979 + from think.event_formatter import format_events 980 980 981 981 entries = [ 982 982 { ··· 1008 1008 1009 1009 def test_format_events_planned_labels(self): 1010 1010 """Test that planned future events use 'Planned', 'Scheduled', 'Expected' labels.""" 1011 - from think.events import format_events 1011 + from think.event_formatter import format_events 1012 1012 1013 1013 entries = [ 1014 1014 { ··· 1031 1031 1032 1032 def test_format_events_occurrence_no_created_on(self): 1033 1033 """Test that occurrences do NOT show 'Created on' or 'Planned' prefix.""" 1034 - from think.events import format_events 1034 + from think.event_formatter import format_events 1035 1035 1036 1036 entries = [ 1037 1037 { ··· 1055 1055 1056 1056 def test_format_events_header_facet_from_path(self): 1057 1057 """Test that facet name and day are extracted from file path.""" 1058 - from think.events import format_events 1058 + from think.event_formatter import format_events 1059 1059 1060 1060 entries = [{"type": "task", "title": "Test", "occurred": True}] 1061 1061 context = {"file_path": "/journal/facets/personal/events/20251215.jsonl"} ··· 1066 1066 1067 1067 def test_format_events_timestamp_calculation(self): 1068 1068 """Test that timestamp is calculated from day + start time.""" 1069 - from think.events import format_events 1069 + from think.event_formatter import format_events 1070 1070 1071 1071 entries = [ 1072 1072 { ··· 1091 1091 1092 1092 def test_format_events_skipped_entries_error(self): 1093 1093 """Test that entries without 'title' field are skipped and reported.""" 1094 - from think.events import format_events 1094 + from think.event_formatter import format_events 1095 1095 1096 1096 entries = [ 1097 1097 {"type": "meeting", "title": "Valid", "occurred": True}, ··· 1108 1108 1109 1109 def test_format_events_mixed_occurred_anticipated(self): 1110 1110 """Test header counts for mixed occurred/anticipated events.""" 1111 - from think.events import format_events 1111 + from think.event_formatter import format_events 1112 1112 1113 1113 entries = [ 1114 1114 {"type": "meeting", "title": "Past event", "occurred": True}, ··· 1123 1123 1124 1124 def test_format_events_time_display_24h(self): 1125 1125 """Test that times are displayed in 24-hour format without seconds.""" 1126 - from think.events import format_events 1126 + from think.event_formatter import format_events 1127 1127 1128 1128 entries = [ 1129 1129 { ··· 1143 1143 1144 1144 def test_format_events_with_details(self): 1145 1145 """Test that details field is included in output.""" 1146 - from think.events import format_events 1146 + from think.event_formatter import format_events 1147 1147 1148 1148 entries = [ 1149 1149 { ··· 1515 1515 1516 1516 def test_format_events_returns_indexer(self): 1517 1517 """Test format_events returns indexer with agent.""" 1518 - from think.events import format_events 1518 + from think.event_formatter import format_events 1519 1519 1520 1520 entries = [{"type": "meeting", "title": "Test", "occurred": True}] 1521 1521 chunks, meta = format_events(entries) ··· 1621 1621 1622 1622 def test_format_events_returns_source(self): 1623 1623 """Test format_events returns source with original event.""" 1624 - from think.events import format_events 1624 + from think.event_formatter import format_events 1625 1625 1626 1626 event = {"type": "meeting", "title": "Test", "occurred": True, "custom": "data"} 1627 1627 entries = [event]
think/events.py think/event_formatter.py
+1 -1
think/formatters.py
··· 139 139 "format_entity_identity", 140 140 False, # Indexed via _index_entity_search_chunks (enriched with relationship data) 141 141 ), 142 - "facets/*/events/*.jsonl": ("think.events", "format_events", True), 142 + "facets/*/events/*.jsonl": ("think.event_formatter", "format_events", True), 143 143 "facets/*/activities/*.jsonl": ("think.activities", "format_activities", True), 144 144 "facets/*/todos/*.jsonl": ("apps.todos.todo", "format_todos", True), 145 145 "facets/*/logs/*.jsonl": ("think.facets", "format_logs", True),
-2
think/indexer/__init__.py
··· 13 13 from .journal import ( 14 14 get_entity_intelligence, 15 15 get_entity_strength, 16 - get_events, 17 16 get_journal_index, 18 17 index_file, 19 18 reset_journal_index, ··· 27 26 # All public functions and constants 28 27 __all__ = [ 29 28 # Journal (unified index) 30 - "get_events", 31 29 "get_entity_intelligence", 32 30 "get_entity_strength", 33 31 "get_journal_index",
-32
think/tools/call.py
··· 37 37 get_import_details, 38 38 list_import_timestamps, 39 39 ) 40 - from think.indexer.journal import get_events as get_events_impl 41 40 from think.indexer.journal import search_counts as search_counts_impl 42 41 from think.indexer.journal import search_journal as search_journal_impl 43 42 from think.utils import ( ··· 128 127 f"\n--- {meta['day']} | {meta['facet']} | {meta['agent']}{stream_tag} | {r['id']} ---" 129 128 ) 130 129 typer.echo(r["text"].strip()) 131 - 132 - 133 - @app.command() 134 - def events( 135 - day: str | None = typer.Argument( 136 - default=None, help="Day YYYYMMDD (default: SOL_DAY env)." 137 - ), 138 - facet: str | None = typer.Option(None, "--facet", "-f", help="Filter by facet."), 139 - ) -> None: 140 - """List events for a day.""" 141 - day = resolve_sol_day(day) 142 - items = get_events_impl(day, facet) 143 - if not items: 144 - typer.echo("No events found.") 145 - return 146 - for ev in items: 147 - time_range = "" 148 - if ev.get("start"): 149 - time_range = ev["start"] 150 - if ev.get("end"): 151 - time_range += f"-{ev['end']}" 152 - time_range = f" ({time_range})" 153 - facet_tag = f" [{ev.get('facet', '')}]" if ev.get("facet") else "" 154 - typer.echo(f"- {ev.get('title', 'Untitled')}{time_range}{facet_tag}") 155 - if ev.get("summary"): 156 - typer.echo(f" {ev['summary']}") 157 - participants = ev.get("participants", []) 158 - if participants: 159 - typer.echo(f" Participants: {', '.join(participants)}") 160 - if ev.get("details"): 161 - typer.echo(f" Details: {ev['details']}") 162 130 163 131 164 132 @facet_app.command()
-39
think/tools/search.py
··· 10 10 from datetime import datetime, timedelta 11 11 from typing import Any 12 12 13 - from think.indexer.journal import get_events as get_events_impl 14 13 from think.indexer.journal import search_counts as search_counts_impl 15 14 from think.indexer.journal import search_journal as search_journal_impl 16 15 ··· 200 199 "error": f"Failed to search journal: {exc}", 201 200 "suggestion": "try adjusting the query or ensure the index exists (run sol indexer --rescan)", 202 201 } 203 - 204 - 205 - def get_events( 206 - day: str, 207 - facet: str | None = None, 208 - ) -> dict[str, Any]: 209 - """Get structured events for a specific day. 210 - 211 - This tool retrieves full event data including titles, summaries, 212 - start/end times, and participants. Use this when you need complete 213 - event information rather than text search results. 214 - 215 - Args: 216 - day: Day in ``YYYYMMDD`` format 217 - facet: Optional facet name to filter by 218 - 219 - Returns: 220 - Dictionary containing: 221 - - day: The requested day 222 - - facet: The facet filter (if any) 223 - - events: List of event objects with full structured data 224 - 225 - Examples: 226 - - get_events("20240101") 227 - - get_events("20240101", facet="work") 228 - """ 229 - try: 230 - events = get_events_impl(day, facet) 231 - return { 232 - "day": day, 233 - "facet": facet or "", 234 - "events": events, 235 - } 236 - except Exception as exc: 237 - return { 238 - "error": f"Failed to get events: {exc}", 239 - "suggestion": "verify the day parameter is valid", 240 - }