From e2bb9fb7178291175f6208a75f01e216884b77ba Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:23:07 +0400 Subject: [PATCH] fix: clear chat sessions on cwd change (#172) --- docs/how-to/chat-sessions.md | 4 ++++ src/takopi/telegram/chat_sessions.py | 17 +++++++++++++++ src/takopi/telegram/loop.py | 9 ++++++++ tests/test_telegram_chat_sessions.py | 31 ++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/docs/how-to/chat-sessions.md b/docs/how-to/chat-sessions.md index 4be7644..c89fc9a 100644 --- a/docs/how-to/chat-sessions.md +++ b/docs/how-to/chat-sessions.md @@ -57,6 +57,10 @@ If you prefer a cleaner chat, hide resume lines: In group chats, Takopi stores a session per sender, so different people can work independently in the same chat. +## Working directory changes + +When `session_mode = "chat"` is enabled, Takopi clears stored chat sessions on startup if the current working directory differs from the one recorded in `telegram_chat_sessions_state.json`. This avoids resuming directory-bound sessions from a different project. + ## Related - [Conversation modes](../tutorials/conversation-modes.md) diff --git a/src/takopi/telegram/chat_sessions.py b/src/takopi/telegram/chat_sessions.py index 6acc1f2..8bba540 100644 --- a/src/takopi/telegram/chat_sessions.py +++ b/src/takopi/telegram/chat_sessions.py @@ -24,6 +24,7 @@ class _ChatState(msgspec.Struct, forbid_unknown_fields=False): class _ChatSessionsState(msgspec.Struct, forbid_unknown_fields=False): version: int + cwd: str | None = None chats: dict[str, _ChatState] = msgspec.field(default_factory=dict) @@ -64,11 +65,27 @@ class ChatSessionStore(JsonStateStore[_ChatSessionsState]): return None return ResumeToken(engine=engine, value=entry.resume) + async def sync_startup_cwd(self, cwd: Path) -> bool: + normalized = str(cwd.expanduser().resolve()) + async with self._lock: + self._reload_locked_if_needed() + previous = self._state.cwd + cleared = False + if previous is not None and previous != normalized: + self._state.chats = {} + cleared = True + if previous != normalized: + self._state.cwd = normalized + self._save_locked() + return cleared + async def set_session_resume( self, chat_id: int, owner_id: int | None, token: ResumeToken ) -> None: async with self._lock: self._reload_locked_if_needed() + if self._state.cwd is None: + self._state.cwd = str(Path.cwd().expanduser().resolve()) chat = self._ensure_chat_locked(chat_id, owner_id) chat.sessions[token.engine] = _SessionState(resume=token.value) self._save_locked() diff --git a/src/takopi/telegram/loop.py b/src/takopi/telegram/loop.py index 73e537c..c32187e 100644 --- a/src/takopi/telegram/loop.py +++ b/src/takopi/telegram/loop.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import AsyncIterator, Awaitable, Callable, Mapping from dataclasses import dataclass from functools import partial +from pathlib import Path from typing import TYPE_CHECKING, cast import anyio @@ -964,6 +965,14 @@ async def run_main_loop( state.chat_session_store = ChatSessionStore( resolve_sessions_path(config_path) ) + cleared = await state.chat_session_store.sync_startup_cwd(Path.cwd()) + if cleared: + logger.info( + "chat_sessions.cleared", + reason="startup_cwd_changed", + cwd=str(Path.cwd()), + state_path=str(resolve_sessions_path(config_path)), + ) logger.info( "chat_sessions.enabled", state_path=str(resolve_sessions_path(config_path)), diff --git a/tests/test_telegram_chat_sessions.py b/tests/test_telegram_chat_sessions.py index 5385222..7d81479 100644 --- a/tests/test_telegram_chat_sessions.py +++ b/tests/test_telegram_chat_sessions.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from takopi.model import ResumeToken @@ -36,3 +38,32 @@ async def test_chat_sessions_store_clear(tmp_path) -> None: engine="codex", value="two", ) + + +@pytest.mark.anyio +async def test_chat_sessions_store_drops_sessions_on_cwd_change( + tmp_path, monkeypatch +) -> None: + path = tmp_path / "telegram_chat_sessions_state.json" + dir1 = tmp_path / "dir1" + dir2 = tmp_path / "dir2" + dir1.mkdir() + dir2.mkdir() + + monkeypatch.chdir(dir1) + store = ChatSessionStore(path) + await store.set_session_resume(1, None, ResumeToken(engine="codex", value="abc123")) + assert await store.get_session_resume(1, None, "codex") == ResumeToken( + engine="codex", value="abc123" + ) + + store2 = ChatSessionStore(path) + assert await store2.sync_startup_cwd(Path.cwd()) is False + assert await store2.get_session_resume(1, None, "codex") == ResumeToken( + engine="codex", value="abc123" + ) + + monkeypatch.chdir(dir2) + store3 = ChatSessionStore(path) + assert await store3.sync_startup_cwd(Path.cwd()) is True + assert await store3.get_session_resume(1, None, "codex") is None