feat(telegram,core): master topic instead of general in private chats

This commit is contained in:
hh
2026-08-28 18:12:58 +02:00
parent 45a5eb4a94
commit b0f280b3ca
3 changed files with 93 additions and 18 deletions
@@ -1,6 +1,8 @@
"""``TelegramFrontend`` - the private chat with the bot as the window (§3.8).
General is the master, a topic is a branch. The user makes a topic and the
A private chat with topics has no General: the gateway makes one topic for
the master (``master_topic``) and rebinds it to every new master; any other
topic is a branch. The user makes a topic and the
first message in it spawns the branch (``seed=morning``); a message into a
topic whose branch is merged or closed spawns a new branch on the same
topic. Replies stream as drafts and land through the outbox; turns that
@@ -112,6 +114,7 @@ class TelegramFrontend(Frontend):
chat_id: int | None = None,
attachments: Attachments = EPHEMERAL,
draft_interval: float = 0.7,
master_topic: str = "🦫 General",
queued_reaction: str = "👀",
poll_timeout: int = 30,
outbox_backoff: float = 2.0,
@@ -122,6 +125,7 @@ class TelegramFrontend(Frontend):
self.master_agent = master_agent
self.branch_agent = branch_agent
self.attachments = attachments
self.master_topic = master_topic
self.draft_interval = draft_interval
self.queued_reaction = queued_reaction
self.poll_timeout = poll_timeout
@@ -131,6 +135,7 @@ class TelegramFrontend(Frontend):
self._inbox: Inbox | None = None
self._outbox: Outbox | None = None
self._targets: dict[str, tuple[int, int | None] | None] = {}
self._master_window: str | None = None
self._topic_names: dict[int, str] = {}
self._drafts: dict[str, Draft] = {}
self._asks: dict[str, _Ask] = {}
@@ -179,7 +184,7 @@ class TelegramFrontend(Frontend):
async def materialize(self, conv: Conversation) -> ConversationBinding | None:
if conv.kind == "master":
return await self.conversations.bind(
conv, frontend=FRONTEND, external_id=self._ext(None)
conv, frontend=FRONTEND, external_id=await self._master_ext()
)
if conv.kind != "branch":
return None
@@ -278,8 +283,30 @@ class TelegramFrontend(Frontend):
dedupe_key=key,
)
async def _master_ext(self) -> str:
"""The master's window.
General in a forum group, our own topic in a private chat (Telegram
has no General there). Created once, then found through whatever
master used it last.
"""
if self._master_window is not None:
return self._master_window
last = await self.conversations.last_binding(frontend=FRONTEND, kind="master")
if last is not None:
self._master_window = last.external_id
elif self.chat_id < 0:
self._master_window = self._ext(None)
else:
topic = await self.bot.create_forum_topic(
self.chat_id, name=self.master_topic[:128]
)
self._topic_names[topic.message_thread_id] = topic.name
self._master_window = self._ext(topic.message_thread_id)
return self._master_window
async def _master(self) -> Conversation:
ext = self._ext(None)
ext = await self._master_ext()
conv = await self.conversations.find_bound(frontend=FRONTEND, external_id=ext)
if conv is not None and conv.status == "open":
return conv
@@ -329,8 +356,11 @@ class TelegramFrontend(Frontend):
return
in_topic = message.is_topic_message or message.forum_topic_created is not None
thread_id = message.message_thread_id if in_topic else None
is_master = (
thread_id is None or self._ext(thread_id) == await self._master_ext()
)
if message.forum_topic_created is not None and thread_id is not None:
if await self._live(thread_id) is None:
if not is_master and await self._live(thread_id) is None:
await self._branch(
thread_id, title=message.forum_topic_created.name, text=None
)
@@ -351,7 +381,7 @@ class TelegramFrontend(Frontend):
if command in _COMMANDS:
await self._command(command, args.strip(), message, thread_id)
return
if thread_id is None:
if is_master:
conv = await self._master()
else:
conv = await self._live(thread_id)
@@ -489,9 +519,10 @@ class TelegramFrontend(Frontend):
async def _run_command(self, command: str, args: str, thread_id: int | None) -> str:
if command in ("start", "help"):
return _HELP
conv = (
await self._master() if thread_id is None else await self._live(thread_id)
is_master = (
thread_id is None or self._ext(thread_id) == await self._master_ext()
)
conv = await self._master() if is_master else await self._live(thread_id)
if command == "status":
return await self._status(conv)
if command == "merge":
@@ -500,7 +531,7 @@ class TelegramFrontend(Frontend):
self._spawn_task(self._merge(conv))
return "🔀 сливаю в мастер…"
if command == "new":
if thread_id is None:
if is_master or thread_id is None:
child = await self.conversations.spawn(
kind="branch",
seed="morning",