From 07f2c1b41eb7f7690501700359f06dd06da08c05 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone <8633216+AlexanderWhitestone@users.noreply.github.com> Date: Wed, 11 Mar 2026 08:47:57 -0400 Subject: [PATCH] fix: wire up tick engine scheduler + add journal + systemd timer (#163) --- deploy/timmy-tick.service | 17 +++++++++++++++++ deploy/timmy-tick.timer | 11 +++++++++++ src/dashboard/app.py | 19 ++++++++++++++++++- src/timmy/cli.py | 14 ++++++++++++++ src/timmy/thinking.py | 23 +++++++++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 deploy/timmy-tick.service create mode 100644 deploy/timmy-tick.timer diff --git a/deploy/timmy-tick.service b/deploy/timmy-tick.service new file mode 100644 index 0000000..9c94957 --- /dev/null +++ b/deploy/timmy-tick.service @@ -0,0 +1,17 @@ +[Unit] +Description=Timmy Tick — one autonomous thinking cycle +Documentation=https://github.com/AlexanderWhitestone/Timmy-time-dashboard +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +WorkingDirectory=/opt/timmy +EnvironmentFile=-/opt/timmy/.env +ExecStart=/opt/timmy/.venv/bin/timmy tick +TimeoutStartSec=120 + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ReadWritePaths=/opt/timmy/data diff --git a/deploy/timmy-tick.timer b/deploy/timmy-tick.timer new file mode 100644 index 0000000..3d7f83e --- /dev/null +++ b/deploy/timmy-tick.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Timmy thinking timer — every 5 minutes +Documentation=https://github.com/AlexanderWhitestone/Timmy-time-dashboard + +[Timer] +OnBootSec=30 +OnUnitActiveSec=5min +AccuracySec=30s + +[Install] +WantedBy=timers.target diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 860cbf8..c9c205b 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -120,6 +120,22 @@ async def _briefing_scheduler() -> None: await asyncio.sleep(_BRIEFING_INTERVAL_HOURS * 3600) +async def _thinking_scheduler() -> None: + """Background task: execute Timmy's thinking cycle every N seconds.""" + from timmy.thinking import thinking_engine + + await asyncio.sleep(5) # Stagger after briefing scheduler + + while True: + try: + if settings.thinking_enabled: + await thinking_engine.think_once() + except Exception as exc: + logger.error("Thinking scheduler error: %s", exc) + + await asyncio.sleep(settings.thinking_interval_seconds) + + async def _start_chat_integrations_background() -> None: """Background task: start chat integrations without blocking startup.""" from integrations.chat_bridge.registry import platform_registry @@ -211,6 +227,7 @@ async def lifespan(app: FastAPI): # Create all background tasks without waiting for them briefing_task = asyncio.create_task(_briefing_scheduler()) + thinking_task = asyncio.create_task(_thinking_scheduler()) # Initialize Spark Intelligence engine from spark.engine import get_spark_engine @@ -266,7 +283,7 @@ async def lifespan(app: FastAPI): await discord_bot.stop() await telegram_bot.stop() - for task in [briefing_task, chat_task]: + for task in [briefing_task, thinking_task, chat_task]: if task: task.cancel() try: diff --git a/src/timmy/cli.py b/src/timmy/cli.py index 5af56be..752c561 100644 --- a/src/timmy/cli.py +++ b/src/timmy/cli.py @@ -23,6 +23,20 @@ _MODEL_SIZE_OPTION = typer.Option( ) +@app.command() +def tick(): + """Run one autonomous thinking cycle (used by systemd timer).""" + import asyncio + + from timmy.thinking import thinking_engine + + thought = asyncio.run(thinking_engine.think_once()) + if thought: + typer.echo(f"[{thought.seed_type}] {thought.content}") + else: + typer.echo("No thought produced (thinking disabled or Ollama down).") + + @app.command() def think( topic: str = typer.Argument(..., help="Topic to reason about"), diff --git a/src/timmy/thinking.py b/src/timmy/thinking.py index 6b0646b..4ee2b43 100644 --- a/src/timmy/thinking.py +++ b/src/timmy/thinking.py @@ -163,6 +163,9 @@ class ThinkingEngine: # Log to swarm event system self._log_event(thought) + # Append to daily journal file + self._write_journal(thought) + # Broadcast to WebSocket clients await self._broadcast(thought) @@ -345,6 +348,26 @@ class ThinkingEngine: except Exception as exc: logger.debug("Failed to log thought event: %s", exc) + def _write_journal(self, thought: Thought) -> None: + """Append the thought to a daily markdown journal file. + + Writes to data/journal/YYYY-MM-DD.md — one file per day, append-only. + """ + try: + ts = datetime.fromisoformat(thought.created_at) + journal_dir = self._db_path.parent / "journal" + journal_dir.mkdir(parents=True, exist_ok=True) + + journal_file = journal_dir / f"{ts.strftime('%Y-%m-%d')}.md" + time_str = ts.strftime("%I:%M %p").lstrip("0") + + entry = f"## {time_str} — {thought.seed_type}\n\n{thought.content}\n\n---\n\n" + + with open(journal_file, "a", encoding="utf-8") as f: + f.write(entry) + except Exception as exc: + logger.debug("Failed to write journal entry: %s", exc) + async def _broadcast(self, thought: Thought) -> None: """Broadcast the thought to WebSocket clients.""" try: