diff --git a/src/config.py b/src/config.py index 33a65a9..1c5bfc4 100644 --- a/src/config.py +++ b/src/config.py @@ -245,6 +245,7 @@ class Settings(BaseSettings): thinking_distill_every: int = 10 # distill facts from thoughts every Nth thought thinking_issue_every: int = 20 # file Gitea issues from thoughts every Nth thought thinking_memory_check_every: int = 50 # check memory status every Nth thought + thinking_idle_timeout_minutes: int = 60 # pause thoughts after N minutes without user input # ── Gitea Integration ───────────────────────────────────────────── # Local Gitea instance for issue tracking and self-improvement. diff --git a/src/dashboard/routes/agents.py b/src/dashboard/routes/agents.py index 31bd7ed..ac2e00b 100644 --- a/src/dashboard/routes/agents.py +++ b/src/dashboard/routes/agents.py @@ -85,6 +85,14 @@ async def chat_agent(request: Request, message: str = Form(...)): raise HTTPException(status_code=422, detail="Message too long") + # Record user activity so the thinking engine knows we're not idle + try: + from timmy.thinking import thinking_engine + + thinking_engine.record_user_input() + except Exception: + pass + timestamp = datetime.now().strftime("%H:%M:%S") response_text = None error_text = None diff --git a/src/dashboard/routes/chat_api.py b/src/dashboard/routes/chat_api.py index 9e047a9..b77fbeb 100644 --- a/src/dashboard/routes/chat_api.py +++ b/src/dashboard/routes/chat_api.py @@ -79,6 +79,14 @@ async def api_chat(request: Request): if not last_user_msg: return JSONResponse(status_code=400, content={"error": "No user message found"}) + # Record user activity so the thinking engine knows we're not idle + try: + from timmy.thinking import thinking_engine + + thinking_engine.record_user_input() + except Exception: + pass + timestamp = datetime.now().strftime("%H:%M:%S") try: diff --git a/src/timmy/thinking.py b/src/timmy/thinking.py index 7ac8a57..9a87265 100644 --- a/src/timmy/thinking.py +++ b/src/timmy/thinking.py @@ -210,6 +210,7 @@ class ThinkingEngine: def __init__(self, db_path: Path = _DEFAULT_DB) -> None: self._db_path = db_path self._last_thought_id: str | None = None + self._last_input_time: datetime = datetime.now(UTC) # Load the most recent thought for chain continuity try: @@ -220,6 +221,17 @@ class ThinkingEngine: logger.debug("Failed to load recent thought: %s", exc) pass # Fresh start if DB doesn't exist yet + def record_user_input(self) -> None: + """Record that a user interaction occurred, resetting the idle timer.""" + self._last_input_time = datetime.now(UTC) + + def _is_idle(self) -> bool: + """Return True if no user input has occurred within the idle timeout.""" + timeout = settings.thinking_idle_timeout_minutes + if timeout <= 0: + return False # Disabled — never idle + return datetime.now(UTC) - self._last_input_time > timedelta(minutes=timeout) + async def think_once(self, prompt: str | None = None) -> Thought | None: """Execute one thinking cycle. @@ -237,6 +249,14 @@ class ThinkingEngine: if not settings.thinking_enabled: return None + # Skip idle periods — don't count internal processing as thoughts + if not prompt and self._is_idle(): + logger.debug( + "Thinking paused — no user input for %d minutes", + settings.thinking_idle_timeout_minutes, + ) + return None + memory_context = self._load_memory_context() system_context = self._gather_system_snapshot() recent_thoughts = self.get_recent_thoughts(limit=5)