feat(deploy): finalize MVP deployment and file transfer approach

This commit is contained in:
Mikhail Putilovskij 2026-05-02 23:45:52 +03:00
parent 6369721876
commit 0f79494fbe
43 changed files with 3078 additions and 645 deletions

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path
from typing import Literal
import yaml
@ -19,6 +20,16 @@ class AgentDefinition:
workspace_path: str = field(default="")
@dataclass(frozen=True)
class AgentAssignment:
agent_id: str | None
source: Literal["configured", "default", "none"]
@property
def is_default(self) -> bool:
return self.source == "default"
class AgentRegistry:
def __init__(
self,
@ -38,6 +49,14 @@ class AgentRegistry:
def get_agent_id_for_user(self, matrix_user_id: str) -> str | None:
return self._user_agents.get(matrix_user_id)
def resolve_agent_for_user(self, matrix_user_id: str) -> AgentAssignment:
agent_id = self.get_agent_id_for_user(matrix_user_id)
if agent_id is not None:
return AgentAssignment(agent_id=agent_id, source="configured")
if self.agents:
return AgentAssignment(agent_id=self.agents[0].agent_id, source="default")
return AgentAssignment(agent_id=None, source="none")
def _required_text(entry: Mapping[str, object], key: str) -> str:
value = entry.get(key)

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import logging
import os
import re
from dataclasses import dataclass
@ -24,21 +25,26 @@ from nio import (
)
from nio.responses import SyncResponse
from adapter.matrix.agent_registry import AgentRegistry, AgentRegistryError, load_agent_registry
from adapter.matrix.converter import from_room_event
from adapter.matrix.files import (
download_matrix_attachment,
matrix_msgtype_for_attachment,
resolve_workspace_attachment_path,
)
from adapter.matrix.agent_registry import AgentRegistry, AgentRegistryError, load_agent_registry
from adapter.matrix.handlers import register_matrix_handlers
from adapter.matrix.handlers.auth import handle_invite, provision_workspace_chat
from adapter.matrix.handlers.auth import (
default_agent_notice,
handle_invite,
provision_workspace_chat,
restore_workspace_access,
)
from adapter.matrix.handlers.context_commands import (
LOAD_PROMPT,
)
from adapter.matrix.routed_platform import RoutedPlatformClient
from adapter.matrix.reconciliation import reconcile_startup_state
from adapter.matrix.room_router import resolve_chat_id
from adapter.matrix.routed_platform import RoutedPlatformClient
from adapter.matrix.store import (
add_staged_attachment,
clear_load_pending,
@ -50,7 +56,6 @@ from adapter.matrix.store import (
remove_staged_attachment_at,
set_pending_confirm,
set_platform_chat_id,
set_room_agent_id,
set_room_meta,
)
from core.auth import AuthManager
@ -118,6 +123,26 @@ def _normalize_agent_base_url(url: str) -> str:
return urlunsplit((parsed.scheme, parsed.netloc, path, "", ""))
def _ws_debug_enabled() -> bool:
value = os.environ.get("SURFACES_DEBUG_WS", "")
return value.strip().lower() in {"1", "true", "yes", "on"}
def _configure_debug_logging() -> None:
if not _ws_debug_enabled():
return
root_logger = logging.getLogger()
if not root_logger.handlers:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)-8s] %(name)s %(message)s",
)
elif root_logger.level > logging.INFO:
root_logger.setLevel(logging.INFO)
logging.getLogger("lambda_agent_api").setLevel(logging.INFO)
logging.getLogger("lambda_agent_api.agent_api").setLevel(logging.INFO)
def _agent_base_url_from_env() -> str:
if base_url := os.environ.get("AGENT_BASE_URL"):
return base_url
@ -135,13 +160,39 @@ def _load_agent_registry_from_env(required: bool = False) -> AgentRegistry | Non
)
return None
try:
return load_agent_registry(registry_path)
registry = load_agent_registry(registry_path)
except (AgentRegistryError, OSError) as exc:
raise RuntimeError(f"failed to load matrix agent registry: {registry_path}") from exc
if _ws_debug_enabled():
logger.warning(
"matrix_agent_registry_loaded",
registry_path=registry_path,
agent_count=len(registry.agents),
)
for agent in registry.agents:
logger.warning(
"matrix_agent_registry_entry",
registry_path=registry_path,
agent_id=agent.agent_id,
label=agent.label,
configured_base_url=agent.base_url,
normalized_base_url=_normalize_agent_base_url(agent.base_url)
if agent.base_url
else "",
workspace_path=agent.workspace_path,
)
return registry
def _build_platform_from_env(*, store: StateStore, chat_mgr: ChatManager) -> PlatformClient:
backend = os.environ.get("MATRIX_PLATFORM_BACKEND", "mock").strip().lower()
if _ws_debug_enabled():
logger.warning(
"matrix_platform_backend_selected",
backend=backend,
global_agent_base_url=_agent_base_url_from_env(),
registry_path=os.environ.get("MATRIX_AGENT_REGISTRY_PATH", "").strip(),
)
if backend == "real":
prototype_state = PrototypeStateStore()
registry = _load_agent_registry_from_env(required=True)
@ -220,6 +271,36 @@ class MatrixBot:
await next_platform_chat_id(self.runtime.store),
)
async def _refresh_room_agent_assignment(
self, room_id: str, matrix_user_id: str, room_meta: dict | None
) -> tuple[dict | None, bool]:
if not room_meta or room_meta.get("redirect_room_id") or self.runtime.registry is None:
return room_meta, False
assignment = self.runtime.registry.resolve_agent_for_user(matrix_user_id)
updated = dict(room_meta)
should_warn_default = False
if assignment.source == "configured" and (
updated.get("agent_id") != assignment.agent_id
or updated.get("agent_assignment") != "configured"
):
updated["agent_id"] = assignment.agent_id
updated["agent_assignment"] = "configured"
updated.pop("default_agent_notice_sent", None)
elif assignment.source == "default":
if not updated.get("agent_id"):
updated["agent_id"] = assignment.agent_id
if updated.get("agent_id") == assignment.agent_id:
updated["agent_assignment"] = "default"
should_warn_default = not updated.get("default_agent_notice_sent")
updated["default_agent_notice_sent"] = True
if updated != room_meta:
await set_room_meta(self.runtime.store, room_id, updated)
return updated, should_warn_default
return room_meta, should_warn_default
async def on_room_message(self, room: MatrixRoom, event: RoomMessageText) -> None:
if getattr(event, "sender", None) == self.client.user_id:
return
@ -228,6 +309,14 @@ class MatrixBot:
room_meta = await get_room_meta(self.runtime.store, room.room_id)
if room_meta is not None and not room_meta.get("redirect_room_id"):
await self._ensure_platform_chat_id(room.room_id, room_meta)
room_meta, warn_default_agent = await self._refresh_room_agent_assignment(
room.room_id, sender, room_meta
)
if warn_default_agent and not body.startswith("!"):
await self._send_all(
room.room_id,
[OutgoingMessage(chat_id=room.room_id, text=default_agent_notice())],
)
load_pending = await get_load_pending(self.runtime.store, sender, room.room_id)
if load_pending is not None and (body.isdigit() or body == "!cancel"):
@ -241,17 +330,97 @@ class MatrixBot:
await self._send_all(room.room_id, outgoing)
return
elif room_meta.get("redirect_room_id"):
display_name = getattr(room, "display_name", None) or sender
if body == "!new":
try:
created = await provision_workspace_chat(
self.client,
sender,
display_name,
self.runtime.platform,
self.runtime.store,
self.runtime.auth_mgr,
self.runtime.chat_mgr,
registry=self.runtime.registry,
)
except Exception as exc:
logger.warning(
"matrix_entry_room_new_chat_failed",
room_id=room.room_id,
sender=sender,
error=str(exc),
)
await self._send_all(
room.room_id,
[
OutgoingMessage(
chat_id=room.room_id,
text="Не удалось создать новый рабочий чат.",
)
],
)
return
welcome = f"Создал новый рабочий чат {created['room_name']}."
if created.get("agent_assignment") == "default":
welcome = f"{welcome}\n\n{default_agent_notice()}"
await self.client.room_send(
created["chat_room_id"],
"m.room.message",
{"msgtype": "m.text", "body": welcome},
)
await set_room_meta(
self.runtime.store,
room.room_id,
{
**room_meta,
"redirect_room_id": created["chat_room_id"],
"redirect_chat_id": created["chat_id"],
},
)
await self._send_all(
room.room_id,
[
OutgoingMessage(
chat_id=room.room_id,
text=(
f"Создал рабочий чат {created['room_name']} "
f"({created['chat_id']}) и отправил приглашение."
),
)
],
)
return
restored = await restore_workspace_access(
self.client,
sender,
display_name,
self.runtime.platform,
self.runtime.store,
self.runtime.auth_mgr,
self.runtime.chat_mgr,
registry=self.runtime.registry,
)
redirect_room_id = room_meta["redirect_room_id"]
redirect_chat_id = room_meta.get("redirect_chat_id", "рабочий чат")
if restored.get("created_new_chat"):
text = (
f"Создал новый рабочий чат {restored['room_name']} "
f"({restored['chat_id']}) и отправил приглашение."
)
else:
text = (
f"Рабочий чат уже создан: {redirect_chat_id}. "
"Я повторно отправил приглашения в пространство Lambda и рабочие чаты. "
"Чтобы создать новый чат, напишите !new здесь."
)
await self._send_all(
room.room_id,
[
OutgoingMessage(
chat_id=room.room_id,
text=(
f"Рабочий чат уже создан: {redirect_chat_id}. "
"Открой приглашённую комнату для продолжения."
),
text=text,
)
],
)
@ -302,6 +471,15 @@ class MatrixBot:
incoming,
)
agent_id = (room_meta or {}).get("agent_id")
if _ws_debug_enabled() and not body.startswith("!"):
logger.warning(
"matrix_incoming_message_route",
room_id=room.room_id,
sender=sender,
local_chat_id=local_chat_id,
agent_id=agent_id,
platform_chat_id=(room_meta or {}).get("platform_chat_id"),
)
workspace_root = self._agent_workspace_root(agent_id)
try:
outgoing = await self.runtime.dispatcher.dispatch(incoming)
@ -520,6 +698,8 @@ class MatrixBot:
f"Привет, {created['user'].display_name or sender}! Пиши — я здесь.\n\n"
"Команды: !new · !chats · !rename · !archive · !context · !save · !load · !help"
)
if created.get("agent_assignment") == "default":
welcome = f"{welcome}\n\n{default_agent_notice()}"
await set_room_meta(
self.runtime.store,
room.room_id,
@ -715,6 +895,7 @@ async def send_outgoing(
async def main() -> None:
_configure_debug_logging()
homeserver = os.environ.get("MATRIX_HOMESERVER")
user_id = os.environ.get("MATRIX_USER_ID")
device_id = os.environ.get("MATRIX_DEVICE_ID", "")
@ -768,6 +949,15 @@ async def main() -> None:
store_path=store_path,
request_timeout=client_config.request_timeout,
)
if _ws_debug_enabled():
logger.warning(
"matrix_ws_debug_enabled",
homeserver=homeserver,
user_id=user_id,
backend=os.environ.get("MATRIX_PLATFORM_BACKEND", "mock").strip().lower(),
global_agent_base_url=_agent_base_url_from_env(),
registry_path=os.environ.get("MATRIX_AGENT_REGISTRY_PATH", "").strip(),
)
try:
await client.sync_forever(timeout=30000, since=since_token)
finally:

View file

@ -2,16 +2,16 @@ from __future__ import annotations
import mimetypes
import re
from datetime import UTC, datetime
from pathlib import Path
from pathlib import Path, PurePosixPath
from core.protocol import Attachment
def _sanitize_component(value: str) -> str:
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", value)
cleaned = cleaned.strip("._-")
return cleaned or "unknown"
def _sanitize_filename(value: str) -> str:
filename = PurePosixPath(str(value).replace("\\", "/")).name.strip()
cleaned = re.sub(r"[\x00-\x1f\x7f<>:\"/\\|?*]+", "_", filename)
cleaned = cleaned.strip(" .")
return cleaned or "attachment.bin"
def _default_filename(attachment: Attachment) -> str:
@ -28,38 +28,38 @@ def _default_filename(attachment: Attachment) -> str:
return f"{base}{extension}"
def build_workspace_attachment_path(
*,
workspace_root: Path,
matrix_user_id: str,
room_id: str,
filename: str,
timestamp: str | None = None,
) -> tuple[str, Path]:
"""Legacy path builder used when no per-agent workspace_path is configured."""
stamp = timestamp or datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
safe_user = _sanitize_component(matrix_user_id.lstrip("@"))
safe_room = _sanitize_component(room_id.lstrip("!"))
safe_name = _sanitize_component(filename) or "attachment.bin"
relative_path = (
Path("surfaces") / "matrix" / safe_user / safe_room / "inbox" / f"{stamp}-{safe_name}"
)
return relative_path.as_posix(), workspace_root / relative_path
def _with_copy_index(filename: str, index: int) -> str:
path = Path(filename)
suffix = path.suffix
stem = path.stem if suffix else filename
return f"{stem} ({index}){suffix}"
def build_agent_incoming_path(
def _unique_workspace_relative_path(workspace_root: Path, filename: str) -> tuple[str, Path]:
safe_name = _sanitize_filename(filename)
candidate = workspace_root / safe_name
if not candidate.exists():
return safe_name, candidate
index = 1
while True:
indexed_name = _with_copy_index(safe_name, index)
candidate = workspace_root / indexed_name
if not candidate.exists():
return indexed_name, candidate
index += 1
def build_agent_workspace_path(
*,
workspace_root: Path,
filename: str,
timestamp: str | None = None,
) -> tuple[str, Path]:
"""Per-agent path builder: saves to {workspace_root}/incoming/{stamp}-{filename}.
"""Saves user files directly to {workspace_root}/{filename}.
The returned relative path is what gets passed to agent.send_message(attachments=[...]).
"""
stamp = timestamp or datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
safe_name = _sanitize_component(filename) or "attachment.bin"
relative_path = Path("incoming") / f"{stamp}-{safe_name}"
return relative_path.as_posix(), workspace_root / relative_path
return _unique_workspace_relative_path(workspace_root, filename)
async def download_matrix_attachment(
@ -76,21 +76,11 @@ async def download_matrix_attachment(
filename = _default_filename(attachment)
if workspace_root.name and str(workspace_root) not in (".", "/workspace", "/agents"):
# Per-agent workspace configured — use simple incoming/ layout
relative_path, absolute_path = build_agent_incoming_path(
workspace_root=workspace_root,
filename=filename,
timestamp=timestamp,
)
else:
relative_path, absolute_path = build_workspace_attachment_path(
workspace_root=workspace_root,
matrix_user_id=matrix_user_id,
room_id=room_id,
filename=filename,
timestamp=timestamp,
)
del matrix_user_id, room_id, timestamp
relative_path, absolute_path = build_agent_workspace_path(
workspace_root=workspace_root,
filename=filename,
)
absolute_path.parent.mkdir(parents=True, exist_ok=True)

View file

@ -22,6 +22,31 @@ def _default_room_name(chat_id: str) -> str:
return f"Чат {suffix}"
def default_agent_notice() -> str:
return (
"Внимание: ваш Matrix ID не найден в конфиге агентов. "
"Пока используется агент по умолчанию. После добавления вас в конфиг "
"бот переключит существующие комнаты на назначенного агента."
)
async def _invite_if_possible(client: Any, room_id: str, matrix_user_id: str) -> bool:
room_invite = getattr(client, "room_invite", None)
if not callable(room_invite):
return False
try:
await room_invite(room_id, matrix_user_id)
return True
except Exception as exc:
logger.warning(
"matrix_workspace_reinvite_failed",
room_id=room_id,
user=matrix_user_id,
error=str(exc),
)
return False
async def provision_workspace_chat(
client: Any,
matrix_user_id: str,
@ -68,10 +93,11 @@ async def provision_workspace_chat(
room_name = room_name_override or _default_room_name(chat_id)
agent_id = None
agent_assignment = "none"
if registry is not None:
agent_id = registry.get_agent_id_for_user(matrix_user_id)
if agent_id is None and registry.agents:
agent_id = registry.agents[0].agent_id
assignment = registry.resolve_agent_for_user(matrix_user_id)
agent_id = assignment.agent_id
agent_assignment = assignment.source
chat_resp = await client.room_create(
name=room_name,
@ -110,6 +136,7 @@ async def provision_workspace_chat(
"space_id": space_id,
"platform_chat_id": platform_chat_id,
"agent_id": agent_id,
"agent_assignment": agent_assignment,
},
)
await chat_mgr.get_or_create(
@ -126,6 +153,64 @@ async def provision_workspace_chat(
"chat_room_id": chat_room_id,
"chat_id": chat_id,
"room_name": room_name,
"agent_assignment": agent_assignment,
"agent_id": agent_id,
}
async def restore_workspace_access(
client: Any,
matrix_user_id: str,
display_name: str,
platform,
store,
auth_mgr,
chat_mgr,
registry: AgentRegistry | None = None,
) -> dict:
user_meta = await get_user_meta(store, matrix_user_id) or {}
space_id = user_meta.get("space_id")
if not space_id:
created = await provision_workspace_chat(
client,
matrix_user_id,
display_name,
platform,
store,
auth_mgr,
chat_mgr,
room_name_override="Чат 1",
registry=registry,
)
return {**created, "reinvited_rooms": [], "created_new_chat": True}
await auth_mgr.confirm(matrix_user_id)
await _invite_if_possible(client, space_id, matrix_user_id)
chats = await chat_mgr.list_active(matrix_user_id)
if not chats:
created = await provision_workspace_chat(
client,
matrix_user_id,
display_name,
platform,
store,
auth_mgr,
chat_mgr,
registry=registry,
)
return {**created, "reinvited_rooms": [], "created_new_chat": True}
reinvited_rooms = []
for chat in chats:
if chat.surface_ref:
if await _invite_if_possible(client, chat.surface_ref, matrix_user_id):
reinvited_rooms.append(chat.surface_ref)
return {
"space_id": space_id,
"reinvited_rooms": reinvited_rooms,
"created_new_chat": False,
}
@ -146,6 +231,29 @@ async def handle_invite(
existing = await get_user_meta(store, matrix_user_id)
if existing and existing.get("space_id"):
restored = await restore_workspace_access(
client,
matrix_user_id,
display_name,
platform,
store,
auth_mgr,
chat_mgr,
registry=registry,
)
body = "Я отправил повторные приглашения в пространство Lambda и рабочие чаты."
if restored.get("created_new_chat"):
body = (
f"Создал новый рабочий чат {restored['room_name']} "
f"({restored['chat_id']}) и отправил приглашение."
)
if restored.get("agent_assignment") == "default":
body = f"{body}\n\n{default_agent_notice()}"
await client.room_send(
room.room_id,
"m.room.message",
{"msgtype": "m.text", "body": body},
)
return
try:
@ -168,6 +276,8 @@ async def handle_invite(
f"Привет, {created['user'].display_name or matrix_user_id}! Пиши — я здесь.\n\n"
"Команды: !new · !chats · !rename · !archive · !clear · !help"
)
if created.get("agent_assignment") == "default":
welcome = f"{welcome}\n\n{default_agent_notice()}"
await client.room_send(
created["chat_room_id"],
"m.room.message",

View file

@ -8,6 +8,7 @@ from nio.api import RoomVisibility
from nio.responses import RoomCreateError
from adapter.matrix.agent_registry import AgentRegistry
from adapter.matrix.handlers.auth import default_agent_notice
from adapter.matrix.store import (
get_user_meta,
next_chat_id,
@ -107,10 +108,11 @@ def make_handle_new_chat(
)
agent_id = None
agent_assignment = "none"
if registry is not None:
agent_id = registry.get_agent_id_for_user(event.user_id)
if agent_id is None and registry.agents:
agent_id = registry.agents[0].agent_id
assignment = registry.resolve_agent_for_user(event.user_id)
agent_id = assignment.agent_id
agent_assignment = assignment.source
room_meta: dict = {
"room_type": "chat",
@ -120,6 +122,7 @@ def make_handle_new_chat(
"space_id": space_id,
"platform_chat_id": platform_chat_id,
"agent_id": agent_id,
"agent_assignment": agent_assignment,
}
await set_room_meta(store, room_id, room_meta)
ctx = await chat_mgr.get_or_create(
@ -129,10 +132,13 @@ def make_handle_new_chat(
surface_ref=room_id,
name=room_name,
)
text = f"Создан чат: {ctx.display_name} ({ctx.chat_id})"
if agent_assignment == "default":
text = f"{text}\n\n{default_agent_notice()}"
return [
OutgoingMessage(
chat_id=event.chat_id,
text=f"Создан чат: {ctx.display_name} ({ctx.chat_id})",
text=text,
)
]

View file

@ -48,7 +48,9 @@ def _chat_id_from_room(room: object, existing_meta: dict | None) -> str | None:
return None
def _space_id_for_room(room: object, rooms_by_id: dict[str, object], existing_meta: dict | None) -> str | None:
def _space_id_for_room(
room: object, rooms_by_id: dict[str, object], existing_meta: dict | None
) -> str | None:
existing_space_id = (existing_meta or {}).get("space_id")
if isinstance(existing_space_id, str) and existing_space_id:
return existing_space_id
@ -69,7 +71,9 @@ def _space_id_for_room(room: object, rooms_by_id: dict[str, object], existing_me
return None
def _matrix_user_id_for_room(room: object, bot_user_id: str | None, existing_meta: dict | None) -> str | None:
def _matrix_user_id_for_room(
room: object, bot_user_id: str | None, existing_meta: dict | None
) -> str | None:
existing_user_id = (existing_meta or {}).get("matrix_user_id")
if isinstance(existing_user_id, str) and existing_user_id:
return existing_user_id
@ -128,11 +132,26 @@ async def reconcile_startup_state(client: object, runtime: object) -> Reconcilia
if not room_meta.get("agent_id"):
registry = getattr(runtime, "registry", None)
if registry is not None:
agent_id = registry.get_agent_id_for_user(matrix_user_id)
if agent_id is None and registry.agents:
agent_id = registry.agents[0].agent_id
if agent_id:
room_meta["agent_id"] = agent_id
assignment = registry.resolve_agent_for_user(matrix_user_id)
if assignment.agent_id:
room_meta["agent_id"] = assignment.agent_id
room_meta["agent_assignment"] = assignment.source
else:
registry = getattr(runtime, "registry", None)
if registry is not None:
assignment = registry.resolve_agent_for_user(matrix_user_id)
if assignment.source == "configured" and (
room_meta.get("agent_id") != assignment.agent_id
or room_meta.get("agent_assignment") != "configured"
):
room_meta["agent_id"] = assignment.agent_id
room_meta["agent_assignment"] = "configured"
elif (
assignment.source == "default"
and room_meta.get("agent_id") == assignment.agent_id
and not room_meta.get("agent_assignment")
):
room_meta["agent_assignment"] = "default"
if existing_meta is None:
result.recovered_rooms += 1
@ -153,7 +172,9 @@ async def reconcile_startup_state(client: object, runtime: object) -> Reconcilia
user_meta = dict(await get_user_meta(runtime.store, matrix_user_id) or {})
user_meta["space_id"] = user_meta.get("space_id") or recovered_space_id
next_chat_index = max_chat_index_by_user[matrix_user_id] + 1
user_meta["next_chat_index"] = max(int(user_meta.get("next_chat_index", 1)), next_chat_index)
user_meta["next_chat_index"] = max(
int(user_meta.get("next_chat_index", 1)), next_chat_index
)
await set_user_meta(runtime.store, matrix_user_id, user_meta)
return result

View file

@ -1,7 +1,10 @@
from __future__ import annotations
import os
from collections.abc import AsyncIterator, Mapping
import structlog
from adapter.matrix.store import get_room_meta
from core.chat import ChatManager
from core.store import StateStore
@ -15,6 +18,13 @@ from sdk.interface import (
UserSettings,
)
logger = structlog.get_logger(__name__)
def _ws_debug_enabled() -> bool:
value = os.environ.get("SURFACES_DEBUG_WS", "")
return value.strip().lower() in {"1", "true", "yes", "on"}
class RoutedPlatformClient(PlatformClient):
def __init__(
@ -77,7 +87,9 @@ class RoutedPlatformClient(PlatformClient):
if callable(close):
await close()
async def _resolve_delegate(self, user_id: str, local_chat_id: str) -> tuple[PlatformClient, str]:
async def _resolve_delegate(
self, user_id: str, local_chat_id: str
) -> tuple[PlatformClient, str]:
chat = await self._chat_mgr.get(local_chat_id, user_id)
if chat is None:
raise PlatformError(
@ -107,4 +119,15 @@ class RoutedPlatformClient(PlatformClient):
code="MATRIX_AGENT_NOT_FOUND",
)
if _ws_debug_enabled():
logger.warning(
"matrix_route_resolved",
user_id=user_id,
local_chat_id=local_chat_id,
surface_ref=chat.surface_ref,
agent_id=str(agent_id),
platform_chat_id=str(platform_chat_id),
delegate_type=type(delegate).__name__,
)
return delegate, str(platform_chat_id)