fix: clear chat sessions on cwd change (#172)

This commit is contained in:
banteg
2026-01-20 13:23:07 +04:00
committed by GitHub
parent 0291a066b3
commit e2bb9fb717
4 changed files with 61 additions and 0 deletions
+4
View File
@@ -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. 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 ## Related
- [Conversation modes](../tutorials/conversation-modes.md) - [Conversation modes](../tutorials/conversation-modes.md)
+17
View File
@@ -24,6 +24,7 @@ class _ChatState(msgspec.Struct, forbid_unknown_fields=False):
class _ChatSessionsState(msgspec.Struct, forbid_unknown_fields=False): class _ChatSessionsState(msgspec.Struct, forbid_unknown_fields=False):
version: int version: int
cwd: str | None = None
chats: dict[str, _ChatState] = msgspec.field(default_factory=dict) chats: dict[str, _ChatState] = msgspec.field(default_factory=dict)
@@ -64,11 +65,27 @@ class ChatSessionStore(JsonStateStore[_ChatSessionsState]):
return None return None
return ResumeToken(engine=engine, value=entry.resume) 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( async def set_session_resume(
self, chat_id: int, owner_id: int | None, token: ResumeToken self, chat_id: int, owner_id: int | None, token: ResumeToken
) -> None: ) -> None:
async with self._lock: async with self._lock:
self._reload_locked_if_needed() 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 = self._ensure_chat_locked(chat_id, owner_id)
chat.sessions[token.engine] = _SessionState(resume=token.value) chat.sessions[token.engine] = _SessionState(resume=token.value)
self._save_locked() self._save_locked()
+9
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
import anyio import anyio
@@ -964,6 +965,14 @@ async def run_main_loop(
state.chat_session_store = ChatSessionStore( state.chat_session_store = ChatSessionStore(
resolve_sessions_path(config_path) 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( logger.info(
"chat_sessions.enabled", "chat_sessions.enabled",
state_path=str(resolve_sessions_path(config_path)), state_path=str(resolve_sessions_path(config_path)),
+31
View File
@@ -1,3 +1,5 @@
from pathlib import Path
import pytest import pytest
from takopi.model import ResumeToken from takopi.model import ResumeToken
@@ -36,3 +38,32 @@ async def test_chat_sessions_store_clear(tmp_path) -> None:
engine="codex", engine="codex",
value="two", 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