diff --git a/CHANGELOG.md b/CHANGELOG.md index d848133fd..3ccef0a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## Unreleased — 2025-12-23 ### Fixes -- Telegram/WhatsApp: native replies now target the original inbound message; reply context is captured in `ReplyTo*` fields for templates. (Thanks @joshp123 for the PR and follow-up question.) +- Telegram/WhatsApp: native replies now target the original inbound message; reply context is appended to `Body` and captured in `ReplyTo*` fields. (Thanks @joshp123 for the PR and follow-up question.) +- Embedded agent: custom model providers now load from `models.providers` (merged into `~/.clawdis/agent/models.json`), enabling proxy/base URL setups. ## 2.0.0-beta2 — 2025-12-21 diff --git a/docs/surface.md b/docs/surface.md index 86107a79a..26827ebc2 100644 --- a/docs/surface.md +++ b/docs/surface.md @@ -10,7 +10,7 @@ Updated: 2025-12-07 Goal: make replies deterministic per channel while keeping one shared context for direct chats. - **Surfaces** (channel labels): `whatsapp`, `webchat`, `telegram`, `voice`, etc. Add `Surface` to inbound `MsgContext` so templates/agents can log which channel a turn came from. Routing is fixed: replies go back to the origin surface; the model doesn’t choose. -- **Reply context (optional):** inbound replies may include `ReplyToId`, `ReplyToBody`, and `ReplyToSender` so templates can surface the quoted context when needed. +- **Reply context:** inbound replies include `ReplyToId`, `ReplyToBody`, and `ReplyToSender`, and the quoted context is appended to `Body` as a `[Replying to ...]` block. - **Canonical direct session:** All direct chats collapse into the single `main` session by default (no config needed). Groups stay `group:`, so they remain isolated. - **Session store:** Keys are resolved via `resolveSessionKey(scope, ctx, mainKey)`; the agent JSONL path lives under `~/.clawdis/sessions/.jsonl`. - **WebChat:** Always attaches to `main`, loads the full session transcript so desktop reflects cross-surface history, and writes new turns back to the same session. diff --git a/docs/telegram.md b/docs/telegram.md index 03c25dad2..3f05b3d28 100644 --- a/docs/telegram.md +++ b/docs/telegram.md @@ -34,7 +34,7 @@ Status: ready for bot-mode use with grammY (long-polling by default; webhook sup ## Planned implementation details - Library: grammY is the only client for send + gateway (fetch fallback removed); grammY throttler is enabled by default to stay under Bot API limits. -- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, `Timestamp`, and reply-to metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`) when the user replies; groups require @bot mention by default. +- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, `Timestamp`, and reply-to metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`) when the user replies; reply context is appended to `Body` as a `[Replying to ...]` block; groups require @bot mention by default. - Outbound: text and media (photo/video/audio/document) with optional caption; chunked to limits. Typing cue sent best-effort. - Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.requireMention`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`, `telegram.webhookPath` supported. diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index a968329ca..e9f3c2348 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -141,7 +141,8 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(1); const payload = replySpy.mock.calls[0][0]; - expect(payload.Body).not.toContain("Reply to Ada: Can you summarize this?"); + expect(payload.Body).toContain("[Replying to Ada]"); + expect(payload.Body).toContain("Can you summarize this?"); expect(payload.ReplyToId).toBe("9001"); expect(payload.ReplyToBody).toBe("Can you summarize this?"); expect(payload.ReplyToSender).toBe("Ada"); diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index d4dfe7a7b..a83651d8b 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -124,13 +124,16 @@ export function createTelegramBot(opts: TelegramBotOptions) { "" ).trim(); if (!rawBody) return; + const replySuffix = replyTarget + ? `\n\n[Replying to ${replyTarget.sender}]\n${replyTarget.body}\n[/Replying]` + : ""; const body = formatAgentEnvelope({ surface: "Telegram", from: isGroup ? buildGroupLabel(msg, chatId) : buildSenderLabel(msg, chatId), timestamp: msg.date ? msg.date * 1000 : undefined, - body: rawBody, + body: `${rawBody}${replySuffix}`, }); const ctxPayload = { diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index 31bb65ce6..640c3fb59 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -1786,10 +1786,13 @@ describe("web auto-reply", () => { ReplyToId?: string; ReplyToBody?: string; ReplyToSender?: string; + Body?: string; }; expect(callArg.ReplyToId).toBe("q1"); expect(callArg.ReplyToBody).toBe("original"); expect(callArg.ReplyToSender).toBe("+1999"); + expect(callArg.Body).toContain("[Replying to +1999]"); + expect(callArg.Body).toContain("original"); }); it("applies responsePrefix to regular replies", async () => { diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index 029292a8f..ea17916e8 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -933,6 +933,12 @@ export async function monitorWebProvider( const backgroundTasks = new Set>(); + const formatReplyContext = (msg: WebInboundMsg) => { + if (!msg.replyToBody) return null; + const sender = msg.replyToSender ?? "unknown sender"; + return `[Replying to ${sender}]\n${msg.replyToBody}\n[/Replying]`; + }; + const buildLine = (msg: WebInboundMsg) => { // Build message prefix: explicit config > default based on allowFrom let messagePrefix = cfg.inbound?.messagePrefix; @@ -945,7 +951,10 @@ export async function monitorWebProvider( msg.chatType === "group" ? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: ` : ""; - const baseLine = `${prefixStr}${senderLabel}${msg.body}`; + const replyContext = formatReplyContext(msg); + const baseLine = `${prefixStr}${senderLabel}${msg.body}${ + replyContext ? `\n\n${replyContext}` : "" + }`; // Wrap with standardized envelope for the agent. return formatAgentEnvelope({