fix: clear chat sessions on cwd change (#172)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user