feat(backends,jobs,telegram,conversations): mcp calls carry the conversation origin, quiet branches close asleep, fresh topics get named

This commit is contained in:
hh committed 2026-10-23 22:24:40 +02:00
1 parent 7d812c7962
commit 944b68e275
9 files changed
+276 -15

No files matched your search

+12 -1
View File
@@ -111,6 +111,7 @@ warnings.filterwarnings("ignore", category=CanUseToolShadowedWarning)
ASK_TOOL = "AskUserQuestion"
__all__ = [
"ORIGIN_HEADER",
"AuditSink",
"ClaudeSdkBackend",
"RunnerConfig",
@@ -120,6 +121,9 @@ __all__ = [
"fingerprint",
]
ORIGIN_HEADER = "X-Conversation-Origin"
"""Rides on every exposed MCP request; the gateway proxy passes it upstream."""
ENV_KEEP: tuple[str, ...] = (
"PATH",
"HOME",
@@ -724,7 +728,14 @@ class ClaudeSdkBackend:
system_prompt = (
prompt_assembly.assemble(sources) if sources else agent.system_prompt
)
servers: dict[str, Any] = dict(self._servers) if spec.tools else {}
servers: dict[str, Any] = (
{
name: {**server, "headers": {ORIGIN_HEADER: key}}
for name, server in self._servers.items()
}
if spec.tools
else {}
)
gateway = (
self._tool_server(key, spec.kind)
if spec.tools and self._tool_server is not None and agent.gateway_tools
+1 -2
View File
@@ -148,8 +148,7 @@ class Spawning(Messaging):
)
if result.text.strip():
await self.inject(parent, result.text, urgency="normal", origin="merge")
await self.set_status(conv, "merged")
await self.mark_closed(conv)
await self.mark_closed(await self.set_status(conv, "merged"))
self._bus.publish(
"conversation.merged",
conversation_id=conv.external_id,
@@ -268,7 +268,10 @@ class TelegramFrontend(Frontend):
)
async def mark_closed(self, conv: Conversation) -> bool:
return await self.mark_topic(conv)
merged = conv.status == "merged"
return await self.mark_topic(
conv, self.texts.closed_prefix if merged else self.texts.idle_prefix
)
async def mark_topic(self, conv: Conversation, prefix: str | None = None) -> bool:
"""Rename the topic; closeForumTopic does not exist in private chats."""
@@ -279,10 +282,7 @@ class TelegramFrontend(Frontend):
name = self._topic_names.get(target[1]) or conv.title or self.texts.branch
if name.startswith(prefix):
return True
await self.bot.edit_forum_topic(
target[0], target[1], name=f"{prefix}{name}"[:128]
)
self._topic_names[target[1]] = f"{prefix}{name}"
await self._rename(target[1], f"{prefix}{self._unmarked(name)}")
return True
def file_note(self) -> str:
@@ -462,6 +462,36 @@ class TelegramFrontend(Frontend):
attachments=attachments,
)
async def _branch_in_topic(
self, topic: int, *, text: str, attachments: list[dict[str, Any]] | None
) -> None:
"""A fresh topic is named by the first line, a closed one loses its mark."""
current = self._topic_names.get(topic)
previous = None
if current is None:
previous = await self.conversations.find_bound(
frontend=FRONTEND, external_id=self._ext(topic)
)
known = current or (previous.title if previous is not None else None)
title = (self._unmarked(known) if known else "") or self._title_from(text)
await self._branch(topic, title=title, text=text, attachments=attachments)
if current == title:
return
try:
await self._rename(topic, title)
except TelegramAPIError as exc:
_log.warning("topic %s not renamed to %r: %s", topic, title, exc)
async def _rename(self, topic: int, name: str) -> None:
name = name[:128]
await self.bot.edit_forum_topic(self.chat_id, topic, name=name)
self._topic_names[topic] = name
def _unmarked(self, name: str) -> str:
for prefix in (self.texts.closed_prefix, self.texts.idle_prefix):
name = name.removeprefix(prefix)
return name
async def _handle(self, update: Update) -> None:
if update.message is not None:
await self._on_message(update.message)
@@ -592,9 +622,8 @@ class TelegramFrontend(Frontend):
topic = cast("int", thread_id)
conv = await self._live(topic)
if conv is None:
title = self._topic_names.get(topic) or self._title_from(text)
await self._branch(
topic, title=title, text=text, attachments=attachments or None
await self._branch_in_topic(
topic, text=text, attachments=attachments or None
)
return
pending = self.conversations.pending_question(conv.external_id)
@@ -42,6 +42,7 @@ class TelegramTexts:
help_head: str = "General is the master, any other topic is a branch."
branch: str = "branch"
closed_prefix: str = "✅ "
idle_prefix: str = "💤 "
waiting: str = "⏳ thinking"
thinking: str = "🤔 thinking"
writing: str = "✍️ writing"
+33 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable, Coroutine
@@ -73,6 +73,23 @@ class JobRun:
)
return True
async def inject_into(
self,
public_id: str | None,
text: str,
*,
urgency: Priority = "normal",
origin: str | None = None,
) -> bool:
"""Inject into that conversation, or the master once it is gone or over."""
conv = await self.conversations.get(public_id) if public_id else None
if conv is None or conv.status != "open":
return await self.inject_master(text, urgency=urgency, origin=origin)
await self.conversations.inject(
conv, text, urgency=urgency, origin=origin or self.job.name
)
return True
async def spawn_job(
self,
*,
@@ -113,6 +130,21 @@ class JobRun:
_log.exception("closing idle %s failed", conv.external_id)
return out
async def close_quiet(
self, *, kind: str = "branch", days: int = 3, limit: int | None = None
) -> list[Conversation]:
"""Close chats quiet for ``days`` without a digest, their windows marked."""
out: list[Conversation] = []
for conv in await self.conversations.idle(kind=kind, days=days, limit=limit):
if await self.conversations.busy(
conv
) or await self.conversations.queue.pending(cast("int", conv.id)):
continue
row = await self.conversations.close(conv)
await self.conversations.mark_closed(row)
out.append(row)
return out
async def retry_in(self, delay: timedelta) -> None:
await self.scheduler.trigger(
self.job, self.payload, delay=delay, trigger=self.trigger