feat(matrix): implement !reset via new platform_chat_id

Instead of calling a /reset endpoint on platform-agent, !reset now
generates a new thread_id (platform_chat_id) for the room. The old
WebSocket connection is closed and the next message creates a fresh
context automatically. No platform changes required.
This commit is contained in:
Mikhail Putilovskij 2026-04-19 21:20:31 +03:00
parent 4a5260ca79
commit 73c472ecc4
4 changed files with 45 additions and 46 deletions

View file

@ -10,6 +10,7 @@ from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_conf
from adapter.matrix.handlers.context_commands import (
make_handle_context,
make_handle_load,
make_handle_reset,
make_handle_save,
)
from adapter.matrix.handlers.settings import (
@ -43,7 +44,7 @@ def register_matrix_handlers(
dispatcher.register(IncomingCommand, "archive", make_handle_archive(client, store))
dispatcher.register(IncomingCommand, "help", handle_help)
dispatcher.register(IncomingCommand, "settings", handle_settings)
dispatcher.register(IncomingCommand, "reset", handle_settings)
dispatcher.register(IncomingCommand, "reset", make_handle_reset(store, prototype_state) if prototype_state is not None else handle_settings)
dispatcher.register(IncomingCommand, "settings_skills", handle_settings_skills)
dispatcher.register(IncomingCommand, "settings_connectors", handle_settings_connectors)
dispatcher.register(IncomingCommand, "settings_soul", handle_settings_soul)

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
import httpx
import structlog
from adapter.matrix.store import get_room_meta, set_load_pending, set_reset_pending
from adapter.matrix.store import get_room_meta, set_load_pending, set_platform_chat_id
from core.protocol import IncomingCommand, OutgoingEvent, OutgoingMessage
if TYPE_CHECKING:
@ -123,23 +123,26 @@ def make_handle_load(store: "StateStore", prototype_state: "PrototypeStateStore"
return handle_load
def make_handle_reset(store: "StateStore", agent_base_url: str):
def make_handle_reset(store: "StateStore", prototype_state: "PrototypeStateStore"):
async def handle_reset(
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
) -> list[OutgoingEvent]:
import time
room_id = await _resolve_room_id(event, chat_mgr)
await set_reset_pending(store, event.user_id, room_id, {"active": True})
return [
OutgoingMessage(
chat_id=event.chat_id,
text=(
"Сбросить контекст агента? Выбери:\n"
" !yes - сбросить\n"
" !save [имя] - сохранить и сбросить\n"
" !no - отмена"
),
)
]
room_meta = await get_room_meta(store, room_id)
old_chat_id = (room_meta or {}).get("platform_chat_id") or room_id
new_chat_id = f"matrix:{room_id}#{int(time.time())}"
await set_platform_chat_id(store, room_id, new_chat_id)
disconnect = getattr(platform, "disconnect_chat", None)
if callable(disconnect):
await disconnect(old_chat_id)
await prototype_state.clear_current_session(new_chat_id)
return [OutgoingMessage(chat_id=event.chat_id, text="Контекст сброшен. Агент не помнит предыдущий разговор.")]
return handle_reset

View file

@ -120,6 +120,15 @@ class RealPlatformClient(PlatformClient):
async def update_settings(self, user_id: str, action) -> None:
await self._prototype_state.update_settings(user_id, action)
async def disconnect_chat(self, chat_id: str) -> None:
chat_key = str(chat_id)
chat_api = self._chat_apis.pop(chat_key, None)
self._chat_send_locks.pop(chat_key, None)
if chat_api is not None:
close = getattr(chat_api, "close", None)
if callable(close):
await close()
async def close(self) -> None:
for chat_api in list(self._chat_apis.values()):
close = getattr(chat_api, "close", None)

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
import httpx
import pytest
from adapter.matrix.bot import MatrixBot, build_runtime
@ -15,7 +15,7 @@ from adapter.matrix.handlers.context_commands import (
)
from adapter.matrix.store import (
get_load_pending,
get_reset_pending,
set_load_pending,
set_room_meta,
)
@ -141,40 +141,26 @@ async def test_load_command_without_saved_sessions_reports_empty():
@pytest.mark.asyncio
async def test_reset_command_shows_dialog_and_sets_pending():
async def test_reset_command_assigns_new_platform_chat_id():
from adapter.matrix.store import get_platform_chat_id, set_room_meta
from sdk.prototype_state import PrototypeStateStore
prototype_state = PrototypeStateStore()
platform = MatrixCommandPlatform()
runtime = build_runtime(platform=platform)
await runtime.chat_mgr.get_or_create(
user_id="u1",
chat_id="C1",
platform="matrix",
surface_ref="!room:example.org",
name="Chat 1",
)
handler = make_handle_reset(store=runtime.store, agent_base_url="http://127.0.0.1:8000")
event = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="reset", args=[])
store = runtime.store
await set_room_meta(store, "!room:example.org", {"platform_chat_id": "matrix:!room:example.org"})
handler = make_handle_reset(store=store, prototype_state=prototype_state)
event = IncomingCommand(user_id="u1", platform="matrix", chat_id="!room:example.org", command="reset", args=[])
result = await handler(event, runtime.auth_mgr, platform, runtime.chat_mgr, runtime.settings_mgr)
assert "!yes" in result[0].text
assert "!save" in result[0].text
assert "!no" in result[0].text
assert await get_reset_pending(runtime.store, "u1", "!room:example.org") == {"active": True}
@pytest.mark.asyncio
async def test_reset_endpoint_unavailable_reports_error():
with patch("adapter.matrix.handlers.context_commands.httpx.AsyncClient") as client_cls:
client = client_cls.return_value
client.__aenter__ = AsyncMock(return_value=client)
client.__aexit__ = AsyncMock(return_value=False)
client.post = AsyncMock(side_effect=httpx.ConnectError("refused"))
from adapter.matrix.handlers.context_commands import _call_reset_endpoint
result = await _call_reset_endpoint("http://127.0.0.1:8000", "!room:example.org")
assert "недоступен" in result[0].text.lower()
new_id = await get_platform_chat_id(store, "!room:example.org")
assert new_id != "matrix:!room:example.org"
assert new_id.startswith("matrix:!room:example.org#")
assert "сброшен" in result[0].text.lower()
@pytest.mark.asyncio