surfaces/.planning/phases/01-matrix-qa-polish/01-RESEARCH.md

33 KiB
Raw Blame History

Phase 1: Matrix QA & Polish — Research

Researched: 2026-04-02 Domain: matrix-nio AsyncClient — Space+rooms architecture, OutgoingUI text rendering, !yes/!no confirmation flow Confidence: HIGH (all critical APIs verified against the installed library)


<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01: Space+rooms — единственная поддерживаемая модель. DM-first убрать.
  • D-02: При первом invite бот создаёт Space Lambda — {display_name}, внутри — первую комнату Чат 1. Приглашает пользователя.
  • D-03: !new [name] создаёт новую комнату внутри Space пользователя, приглашает его туда.
  • D-04: !archive выводит комнату из Space (не удаляет).
  • D-05: Маппинг room_id → space_id + room_id → chat_id хранится в SQLite через adapter/matrix/store.py.
  • D-06: Реакции (👍/) — убрать полностью. OutgoingUI с кнопками рендерится как текст + !yes / !no.
  • D-07: Когда агент запрашивает подтверждение, бот пишет текстовое сообщение с описанием и подсказкой: Ответьте !yes для подтверждения или !no для отмены.
  • D-08: !yes / !no работают в текущей комнате. Состояние ожидания подтверждения хранится per (user_id, room_id).
  • D-09: Все команды работают из любой комнаты Space — нет выделенной комнаты «Настройки».
  • D-10: Команды: !new [name], !chats, !rename <name>, !archive, !skills, !soul, !safety, !settings, !yes, !no.
  • D-11: !start — не нужен, онбординг через invite flow.
  • D-12: !settings — read-only дашборд: одно сообщение со статусом всего (скиллы, soul, safety, активные чаты). Ничего не меняет.
  • D-13: Изменения через субкоманды: !skills, !skill on/off <name>, !soul, !soul name/style/priority/reset <value>, !safety, !safety on/off <action>.
  • D-14: Каждая команда без аргументов (!skills, !soul, !safety) выводит текущее состояние + подсказку как менять.

Claude's Discretion

  • Формат текста в Matrix (plain text vs markdown) — на усмотрение, Matrix клиенты рендерят markdown
  • Структура invite-сообщения в новом Space — на усмотрение, главное: приветствие + список команд
  • Обработка ошибок matrix-nio (room_create fail, join fail) — логировать + ответить пользователю

Deferred Ideas (OUT OF SCOPE)

  • Комната «Настройки» как отдельная закреплённая комната — решили не делать в Phase 1
  • E2EE / python-olm — инфраструктурный трек, вне scope
  • Space discovery (бот находит существующий Space при повторном invite) — Phase 2+
  • Attachment handling (m.file, m.image, m.audio) — Phase 2+ </user_constraints>

Summary

Phase 1 переписывает Matrix адаптер с DM-first на Space+rooms модель, убирает реакции в пользу !yes/!no, и реализует все команды управления. Большая часть бизнес-логики уже работает через core/handlers/ и adapter/matrix/handlers/settings.py. Главная работа — в трёх точках: handle_invite (создание Space + двух комнат), make_handle_new_chat (добавление комнаты в Space), и send_outgoing (убрать реакции, добавить pending-state для !yes/!no).

Текущее состояние: 97 тестов зелёные. Для "96+ зелёных" после рефакторинга нужно обновить 3 существующих теста (они проверяют DM-поведение и реакции) и добавить ~12 новых тестов на Space-сценарии. Итого целевой range — 106110 тестов.

Критическая деталь: AsyncClient.room_create принимает space=True (булевый параметр, не room_type="m.space") для создания Space. Добавление дочерней комнаты — через room_put_state на Space с event_type m.space.child и state_key = child room_id. Это проверено против установленной версии matrix-nio.

Primary recommendation: Реализовать в трёх независимых задачах Codex: (1) invite flow — Space+rooms creation, (2) send_outgoing — убрать реакции, добавить pending-confirm store, (3) обновить тесты под новое поведение.


Standard Stack

Core

Library Version Purpose Why Standard
matrix-nio установлена (проверено: space=True параметр присутствует) Matrix async клиент — room_create, room_put_state, room_invite, join Единственный maintained async Python Matrix клиент
structlog уже используется Логирование Уже в проекте
pytest-asyncio уже используется Async тесты Уже в проекте

Версию matrix-nio не нужно менять. Установленная версия поддерживает space=True в room_create и room_put_state для state events.


Architecture Patterns

Паттерн 1: Создание Space + первой комнаты (invite flow)

Что: При первом invite бот делает 5 последовательных API вызовов — создание Space, создание chat-комнаты, линковка child→Space, приглашение пользователя в обе, запись в store.

Verified API (из installed matrix-nio):

# 1. Создать Space
space_resp = await client.room_create(
    name=f"Lambda — {display_name}",
    space=True,               # <-- булевый флаг, не room_type
    visibility="private",
    is_direct=False,
)
# space_resp.room_id — строка

# 2. Создать первую chat-комнату
chat_resp = await client.room_create(
    name="Чат 1",
    visibility="private",
    is_direct=False,
)
# chat_resp.room_id — строка

# 3. Добавить комнату в Space как child
# state_key = room_id дочерней комнаты
await client.room_put_state(
    room_id=space_resp.room_id,
    event_type="m.space.child",
    content={
        "via": [homeserver_domain],  # например "matrix.org"
    },
    state_key=chat_resp.room_id,
)

# 4. Пригласить пользователя в Space и в chat-комнату
await client.room_invite(space_resp.room_id, matrix_user_id)
await client.room_invite(chat_resp.room_id, matrix_user_id)

# 5. Записать в store
await set_user_meta(store, matrix_user_id, {
    "space_id": space_resp.room_id,
    "next_chat_index": 2,  # C1 уже занят
})
await set_room_meta(store, chat_resp.room_id, {
    "room_type": "chat",
    "chat_id": "C1",
    "display_name": "Чат 1",
    "matrix_user_id": matrix_user_id,
    "space_id": space_resp.room_id,
})

Важный gotcha: Бот сам не вступает в Space (join). Он создаёт Space как владелец, поэтому уже является членом. join нужен только для входящей DM-комнаты (invite в существующую комнату). В новом flow: бот создаёт комнаты сам, поэтому join для Space и chat-комнаты не нужен.

Паттерн 2: Добавление новой комнаты (!new)

async def handle_new_chat(...):
    user_meta = await get_user_meta(store, event.user_id) or {}
    space_id = user_meta.get("space_id")
    if not space_id:
        # Пользователь не прошёл invite flow — не должно случиться, но guard нужен
        return [OutgoingMessage(chat_id=event.chat_id, text="Ошибка: Space не найден.")]
    
    chat_id = await next_chat_id(store, event.user_id)
    room_name = " ".join(event.args).strip() or f"Чат {chat_id}"
    
    resp = await client.room_create(name=room_name, visibility="private", is_direct=False)
    room_id = resp.room_id
    
    homeserver = event.user_id.split(":")[1]  # "@user:matrix.org" → "matrix.org"
    await client.room_put_state(
        room_id=space_id,
        event_type="m.space.child",
        content={"via": [homeserver]},
        state_key=room_id,
    )
    await client.room_invite(room_id, event.user_id)
    await set_room_meta(store, room_id, {
        "room_type": "chat",
        "chat_id": chat_id,
        "display_name": room_name,
        "matrix_user_id": event.user_id,
        "space_id": space_id,
    })

Паттерн 3: Archive (!archive) — убрать из Space

# Убрать child: поставить пустой content (или content без 'via')
# Matrix spec: отправить m.space.child с пустым {} или без 'via' удаляет связь
await client.room_put_state(
    room_id=space_id,
    event_type="m.space.child",
    content={},          # пустой content = удалить child relationship
    state_key=room_id,   # room_id архивируемой комнаты
)

Confidence: MEDIUM — Matrix spec говорит что пустой content убирает child, но поведение Element может варьироваться. Альтернатива: оставить room_put_state с {"via": []} (пустой массив).

Паттерн 4: OutgoingUI → текст + !yes/!no (без реакций)

Что убрать:

  • _button_action_to_reaction в bot.py — удалить целиком
  • Блок for button in event.buttons: reaction = _button_action_to_reaction(...) — удалить
  • ReactionEvent callback (on_reaction + client.add_event_callback) — удалить
  • from_reaction в converter — оставить (используется для skill-reactions), но skill-reaction инфраструктура тоже под вопросом (D-06 убирает реакции полностью)

Что добавить в send_outgoing для OutgoingUI:

if isinstance(event, OutgoingUI):
    lines = [event.text, ""]
    for button in event.buttons:
        lines.append(f"• {button.label}")
    lines += ["", "Ответьте !yes для подтверждения или !no для отмены."]
    body = "\n".join(lines)
    await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})
    # Сохранить pending state per (user_id, room_id)
    await set_pending_confirm(store, user_id=???, room_id=room_id, action_id=???)

Проблема: send_outgoing сейчас не знает user_id — только room_id. Для сохранения pending state нужен либо рефакторинг сигнатуры, либо хранение pending по room_id (без user_id — достаточно, т.к. room_id уникален для конкретного пользователя в Space модели).

Паттерн 5: Pending confirm state

# Новые helpers в adapter/matrix/store.py
PENDING_CONFIRM_PREFIX = "matrix_pending_confirm:"

async def get_pending_confirm(store, room_id: str) -> dict | None:
    return await store.get(f"{PENDING_CONFIRM_PREFIX}{room_id}")

async def set_pending_confirm(store, room_id: str, meta: dict) -> None:
    await store.set(f"{PENDING_CONFIRM_PREFIX}{room_id}", meta)

async def clear_pending_confirm(store, room_id: str) -> None:
    await store.delete(f"{PENDING_CONFIRM_PREFIX}{room_id}")

!yes/!no уже конвертируются в IncomingCallback(action="confirm"/"cancel") в converter.py. Нужно обновить handle_confirm/handle_cancel в adapter/matrix/handlers/confirm.py чтобы читать pending state и возвращать осмысленный ответ.

Паттерн 6: Hardcoded "C1" bug fix

# auth.py:27 — СЕЙЧАС (баг):
"chat_id": "C1"

# ДОЛЖНО БЫТЬ:
chat_id = await next_chat_id(store, matrix_user_id)  # возвращает "C1" для первого пользователя

next_chat_id уже существует в store.py и правильно инкрементирует per-user. Нужно просто использовать его в handle_invite вместо хардкода.

Рекомендуемая структура store после рефакторинга

Текущие ключи в store:

  • matrix_room:{room_id}{room_type, chat_id, display_name, matrix_user_id}добавить space_id
  • matrix_user:{user_id}{next_chat_index, ...}добавить space_id
  • matrix_state:{room_id}{state} — оставить как есть
  • matrix_skills_msg:{room_id}{event_id} — оставить (или убрать если реакции полностью уходят)

Новые ключи:

  • matrix_pending_confirm:{room_id}{action_id, description, expires_at} — для !yes/!no

Don't Hand-Roll

Problem Don't Build Use Instead Why
Space creation Кастомный HTTP запрос к Matrix API AsyncClient.room_create(space=True) Встроено в matrix-nio, управляет session state
Adding child room to Space Кастомный state event builder AsyncClient.room_put_state(room_id, "m.space.child", ...) Правильный Content-Type, auth headers автоматически
User invite Прямой HTTP PUT AsyncClient.room_invite(room_id, user_id) Обрабатывает ошибки M_FORBIDDEN, already-joined
Error detection Проверка статус-кодов isinstance(resp, RoomCreateError) / isinstance(resp, RoomPutStateError) matrix-nio возвращает типизированные error-объекты

Common Pitfalls

Pitfall 1: room_create(space=True) vs room_type="m.space"

What goes wrong: Передача room_type="m.space" как отдельный параметр — работает, но space=True — это удобный shortcut в matrix-nio, который внутри устанавливает тот же room_type. Оба варианта корректны, но space=True проще читается.

Проверено: room_create signature в installed matrix-nio имеет space: bool = False. Нет отдельного is_space параметра.

How to avoid: Использовать space=True, не room_type="m.space".

Pitfall 2: room_id из RoomCreateResponse — не getattr

What goes wrong: Текущий код в handlers/chat.py:55: room_id = getattr(response, "room_id", None). Это работает для RoomCreateResponse, но молча возвращает None если пришёл RoomCreateError (у которого нет room_id).

How to avoid:

from nio.responses import RoomCreateError
resp = await client.room_create(...)
if isinstance(resp, RoomCreateError):
    logger.error("room_create failed", status_code=resp.status_code)
    return [OutgoingMessage(..., text="Не удалось создать комнату.")]
room_id = resp.room_id  # прямой доступ, не getattr

Pitfall 3: m.space.child — state_key это room_id дочерней комнаты, не пустая строка

What goes wrong: room_put_state по умолчанию state_key="". Для m.space.child state_key ДОЛЖЕН быть room_id дочерней комнаты — иначе Space создастся некорректно.

How to avoid: Всегда передавать state_key=child_room_id явно.

Pitfall 4: Бот должен быть в Space чтобы добавлять children

What goes wrong: Бот создаёт Space (становится владельцем), потом пытается сделать room_put_state на Space. Это работает т.к. создатель автоматически имеет power level 100. Но если бот потерял membership (kicked out), room_put_state вернёт M_FORBIDDEN.

How to avoid: Логировать ошибку и сообщать пользователю. Не ретраить молча.

Pitfall 5: Дублирование invite flow (идемпотентность)

What goes wrong: Текущий handle_invite проверяет get_room_meta(store, room.room_id) чтобы не запускать flow дважды. После рефакторинга на Space+rooms нужно проверять get_user_meta(store, matrix_user_id) — потому что invite может прийти повторно в разные комнаты Space, а Space создаётся один раз per user.

How to avoid: Idempotency check переносится на уровень user_meta: if user_meta.get("space_id"): return.

Pitfall 6: skills_message реакции — остаток от старого UX

What goes wrong: adapter/matrix/reactions.py и build_skills_text до сих пор рендерят "Реакции 1-9 переключают навыки." По D-06 реакции убраны полностью. build_skills_text нужно обновить чтобы убрать эту строку и заменить инструкцией !skill on/off <name>.

How to avoid: Обновить build_skills_text + тест test_reactions.py::test_build_skills_text.

Pitfall 7: on_reaction callback остаётся зарегистрированным

What goes wrong: В main() есть client.add_event_callback(bot.on_reaction, ReactionEvent). Если убрать реакции но оставить этот callback — matrix-nio будет продолжать обрабатывать реакции и вызывать on_reaction. Нужно удалить и callback-регистрацию, и импорт ReactionEvent.


Gaps between Current Implementation and Target

File Current State Target State Action
adapter/matrix/handlers/auth.py DM join + hardcoded C1 Space creation + C1 from next_chat_id Переписать handle_invite
adapter/matrix/handlers/chat.py room_create без Space room_create + room_put_state в Space Обновить make_handle_new_chat
adapter/matrix/bot.py on_reaction + _button_action_to_reaction Без реакций, pending-state для !yes/!no Убрать reaction code; обновить send_outgoing
adapter/matrix/store.py Нет space_id, нет pending_confirm space_id в room_meta + user_meta; pending_confirm helpers Добавить поля и helpers
adapter/matrix/reactions.py build_skills_text упоминает реакции build_skills_text без реакций, с !skill on/off Обновить текст
adapter/matrix/handlers/confirm.py Заглушка без state Читает pending_confirm, даёт реальный ответ Обновить handlers
adapter/matrix/handlers/settings.py handle_settings — список команд handle_settings — read-only дашборд (D-12) Обновить до дашборда со статусом
adapter/matrix/converter.py from_reaction используется для skill toggle Skill toggle через реакции убирается from_reaction можно оставить или удалить

Code Examples

Создание Space + child room (verified API)

# Source: matrix-nio installed version — inspect.signature(AsyncClient.room_create)
from nio.responses import RoomCreateError, RoomPutStateError

async def create_user_space(client, display_name: str, matrix_user_id: str, store):
    homeserver = matrix_user_id.split(":")[-1]  # "@user:matrix.org" → "matrix.org"

    # Step 1: Create Space
    space_resp = await client.room_create(
        name=f"Lambda — {display_name}",
        space=True,
        visibility="private",
    )
    if isinstance(space_resp, RoomCreateError):
        return None, None
    space_id = space_resp.room_id

    # Step 2: Create first chat room
    chat_resp = await client.room_create(
        name="Чат 1",
        visibility="private",
        is_direct=False,
    )
    if isinstance(chat_resp, RoomCreateError):
        return space_id, None
    chat_room_id = chat_resp.room_id

    # Step 3: Link child room into Space (state_key = child's room_id)
    await client.room_put_state(
        room_id=space_id,
        event_type="m.space.child",
        content={"via": [homeserver]},
        state_key=chat_room_id,
    )

    # Step 4: Invite user to Space and to chat room
    await client.room_invite(space_id, matrix_user_id)
    await client.room_invite(chat_room_id, matrix_user_id)

    return space_id, chat_room_id

send_outgoing для OutgoingUI (без реакций)

if isinstance(event, OutgoingUI):
    lines = [event.text]
    if event.buttons:
        lines.append("")
        for btn in event.buttons:
            lines.append(f"• {btn.label}")
        lines.append("")
        lines.append("Ответьте !yes для подтверждения или !no для отмены.")
    body = "\n".join(lines)
    await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})

Проверка ошибок matrix-nio

from nio.responses import RoomCreateError, RoomPutStateError, RoomInviteError

resp = await client.room_create(...)
if isinstance(resp, RoomCreateError):
    logger.error("room_create failed", status_code=resp.status_code)
    # resp не имеет room_id — безопасный ранний возврат
    return [OutgoingMessage(chat_id=event.chat_id, text="Не удалось создать комнату.")]
room_id = resp.room_id  # str, гарантированно присутствует

Validation Architecture

nyquist_validation = true в config.json — раздел обязателен.

Test Framework

Property Value
Framework pytest + pytest-asyncio
Config file pytest.ini или pyproject.toml (проверить наличие)
Quick run command pytest tests/adapter/matrix/ -q
Full suite command pytest tests/ -q
Current count 97 passed

Существующие тесты Matrix, требующие обновления

Эти тесты написаны под DM/reaction-based поведение и сломаются после рефакторинга:

Test Текущее поведение После рефакторинга Действие
test_dispatcher.py::test_invite_event_creates_dm_room_and_sends_welcome Проверяет chat_id == "C1" через hardcode, join DM Должен проверять Space creation + chat room creation Переписать
test_dispatcher.py::test_new_chat_creates_real_matrix_room_when_client_available Проверяет room_create без Space Должен проверять room_create + room_put_state Обновить mock + assertions
test_reactions.py::test_build_skills_text Ожидает "Реакции 1-9" в тексте После удаления реакций эта строка исчезнет Обновить assertion
test_reactions.py::test_build_confirmation_text Проверяет CONFIRM_REACTION + "подтвердить" Если build_confirmation_text обновится под D-07 Обновить

Новые тесты, необходимые для покрытия Space+rooms

ID Behavior Test Type File Command
MAT-01 handle_invite создаёт Space + Чат 1, сохраняет space_id в user_meta unit tests/adapter/matrix/test_invite_space.py pytest tests/adapter/matrix/test_invite_space.py -x
MAT-02 handle_invite идемпотентен: повторный вызов не создаёт второй Space unit tests/adapter/matrix/test_invite_space.py pytest tests/adapter/matrix/test_invite_space.py -x
MAT-03 handle_invite использует next_chat_id, не хардкод "C1" unit tests/adapter/matrix/test_invite_space.py pytest tests/adapter/matrix/test_invite_space.py -x
MAT-04 make_handle_new_chat вызывает room_put_state с space_id из user_meta unit tests/adapter/matrix/test_chat_space.py pytest tests/adapter/matrix/test_chat_space.py -x
MAT-05 make_handle_new_chat без space_id возвращает error message unit tests/adapter/matrix/test_chat_space.py pytest tests/adapter/matrix/test_chat_space.py -x
MAT-06 send_outgoing для OutgoingUI рендерит текст + "!yes / !no", без реакций unit tests/adapter/matrix/test_send_outgoing.py pytest tests/adapter/matrix/test_send_outgoing.py -x
MAT-07 send_outgoing для OutgoingUI НЕ отправляет m.reaction event unit tests/adapter/matrix/test_send_outgoing.py pytest tests/adapter/matrix/test_send_outgoing.py -x
MAT-08 get/set/clear_pending_confirm roundtrip в store unit tests/adapter/matrix/test_store.py (extend) pytest tests/adapter/matrix/test_store.py -x
MAT-09 handle_confirm читает pending_confirm и возвращает описание действия unit tests/adapter/matrix/test_confirm.py pytest tests/adapter/matrix/test_confirm.py -x
MAT-10 handle_archive вызывает room_put_state с пустым content unit tests/adapter/matrix/test_chat_space.py pytest tests/adapter/matrix/test_chat_space.py -x
MAT-11 !settings возвращает дашборд со статусом (не список команд) unit tests/adapter/matrix/test_dispatcher.py (extend) pytest tests/adapter/matrix/test_dispatcher.py -x
MAT-12 RoomCreateError обрабатывается корректно (нет crash, есть user message) unit tests/adapter/matrix/test_chat_space.py pytest tests/adapter/matrix/test_chat_space.py -x

Wave 0 Gaps (новые файлы)

  • tests/adapter/matrix/test_invite_space.py — покрывает MAT-01, MAT-02, MAT-03
  • tests/adapter/matrix/test_chat_space.py — покрывает MAT-04, MAT-05, MAT-10, MAT-12
  • tests/adapter/matrix/test_send_outgoing.py — покрывает MAT-06, MAT-07
  • tests/adapter/matrix/test_confirm.py — покрывает MAT-09

Sampling Rate

  • Per task commit: pytest tests/adapter/matrix/ -q
  • Per wave merge: pytest tests/ -q
  • Phase gate: All 97+ tests green (целевой диапазон 106110 после добавления новых)

Численный ориентир для "96+ зелёных"

  • Сейчас: 97 тестов, все зелёные
  • После рефакторинга без добавления тестов: 4 теста сломаются (3 dispatcher + 1 reactions) → ~93 зелёных
  • После обновления сломанных: 97 зелёных
  • После добавления 12 новых: ~109 зелёных
  • Итого: требование "96+" выполнено с запасом

Environment Availability

Dependency Required By Available Version Fallback
matrix-nio All Matrix API calls установлена, space=True присутствует
pytest + pytest-asyncio Test suite работает (97 passed)
SQLite SQLiteStore встроен в Python
Matrix homeserver Manual QA только не проверялось Без homeserver — только unit тесты

Missing dependencies with no fallback: Нет (homeserver нужен только для ручного QA, не для автотестов).


Project Constraints (from CLAUDE.md)

Directive Impact on Phase
core/protocol.py — типы не менять IncomingCommand, OutgoingUI, UIButton используем as-is
Все вызовы платформы через platform/interface.py MockPlatformClient остаётся, SDK не трогать
Хотфиксы < 20 строк → Claude Code напрямую Небольшие правки реакций-в-текст могут идти напрямую
Реализацию делает Codex Три задачи — три параллельных Codex запуска
Blueprint перед реализацией Плану нужны blueprint-документы для каждой задачи
Порядок зависимостей: core/ → platform/ → adapters/ Все изменения только в adapter/matrix/, core/ не трогаем

Open Questions

  1. Стоит ли полностью убирать from_reaction и reactions.py?

    • D-06 говорит "убрать реакции полностью"
    • reactions.py содержит build_confirmation_text и build_skills_text — они нужны после рефакторинга
    • Рекомендация: оставить reactions.py, удалить CONFIRM_REACTION/CANCEL_REACTION/add_reaction/remove_reaction, переименовать в formatting.py — но это необязательно для Phase 1.
  2. Нужен ли m.space.parent event в дочерних комнатах?

    • Matrix spec позволяет устанавливать m.space.parent в дочерней комнате, чтобы Element показывал ссылку "назад к Space"
    • Не является обязательным — m.space.child в Space достаточно для включения комнаты в Space
    • Рекомендация: не добавлять в Phase 1, отложить если понадобится.
  3. via в m.space.child — один сервер или несколько?

    • Для single-homeserver деплоя: ["homeserver_domain"] достаточно
    • Для федерации: нужны несколько серверов
    • Рекомендация: парсить из matrix_user_id.split(":")[-1] — достаточно для текущего использования.

Sources

Primary (HIGH confidence)

  • matrix-nio installed package — AsyncClient.room_create, room_put_state, room_invite, join — сигнатуры и docstrings проверены через inspect.signature и help()
  • nio.responses.RoomCreateResponse, RoomCreateError, RoomPutStateResponse, RoomPutStateError — поля проверены через inspect.getsource
  • Весь codebase прочитан напрямую

Secondary (MEDIUM confidence)

  • Matrix Spec v1.x — m.space.child event format (content {"via": [...]}, state_key = child room_id) — стандартное поведение, описано в Matrix spec

Metadata

Confidence breakdown:

  • matrix-nio API: HIGH — проверено против installed package через Python introspection
  • Space creation pattern: HIGH — space=True параметр подтверждён в room_create signature
  • m.space.child content format: MEDIUM — стандарт Matrix spec, не проверен против конкретного homeserver
  • Archive via empty content: MEDIUM — Matrix spec behaviour, может зависеть от homeserver version
  • Тест-план: HIGH — основан на прямом анализе существующих тестов

Research date: 2026-04-02 Valid until: 2026-05-02 (matrix-nio обновляется редко, Space API стабилен с Matrix v1.2)