From 43198d9dcc31f3de3c0b7f073ba416f4a55761c0 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Mon, 16 Feb 2026 12:24:24 +0800 Subject: [PATCH 1/3] feat(core): add rpc to generate channel welcome messages --- packages/core/src/hub/hub.ts | 2 + .../rpc/handlers/generate-channel-welcome.ts | 105 ++++++++++++++++++ packages/core/src/hub/rpc/index.ts | 1 + 3 files changed, 108 insertions(+) create mode 100644 packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts diff --git a/packages/core/src/hub/hub.ts b/packages/core/src/hub/hub.ts index 30d58cfb..b852905d 100644 --- a/packages/core/src/hub/hub.ts +++ b/packages/core/src/hub/hub.ts @@ -31,6 +31,7 @@ import { createSetHeartbeatsHandler } from "./rpc/handlers/set-heartbeats.js"; import { createWakeHeartbeatHandler } from "./rpc/handlers/wake-heartbeat.js"; import { DeviceStore, type DeviceMeta } from "./device-store.js"; import { createVerifyHandler } from "./rpc/handlers/verify.js"; +import { createGenerateChannelWelcomeHandler } from "./rpc/handlers/generate-channel-welcome.js"; import { ExecApprovalManager } from "./exec-approval-manager.js"; import { createResolveExecApprovalHandler } from "./rpc/handlers/resolve-exec-approval.js"; import { evaluateCommandSafety, requiresApproval } from "../agent/tools/exec-safety.js"; @@ -115,6 +116,7 @@ export class Hub { return this._onConfirmDevice(deviceId, agentId, meta); }, })); + this.rpc.register("generateChannelWelcome", createGenerateChannelWelcomeHandler(this)); this.rpc.register("getAgentMessages", createGetAgentMessagesHandler()); this.rpc.register("getHubInfo", createGetHubInfoHandler(this)); this.rpc.register("listAgents", createListAgentsHandler(this)); diff --git a/packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts b/packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts new file mode 100644 index 00000000..f26fc1e0 --- /dev/null +++ b/packages/core/src/hub/rpc/handlers/generate-channel-welcome.ts @@ -0,0 +1,105 @@ +import type { RpcHandler } from "../dispatcher.js"; +import { RpcError } from "../dispatcher.js"; + +interface GenerateChannelWelcomeParams { + agentId?: string; + channel?: string; + language?: string; + firstName?: string; + isReconnect?: boolean; +} + +interface AgentLike { + runInternalForResult(content: string): Promise<{ text: string; error?: string }>; + closed?: boolean; +} + +interface HubLike { + getAgent(id: string): AgentLike | undefined; +} + +interface WelcomeContext { + channel: string; + language: string; + firstName: string; + isReconnect: boolean; +} + +const DEFAULT_LANGUAGE = "English"; +const DEFAULT_CHANNEL = "telegram"; +const DEFAULT_FIRST_NAME = "there"; + +function normalizeLanguage(value: string | undefined): string { + const trimmed = value?.trim(); + if (!trimmed) return DEFAULT_LANGUAGE; + + const normalized = trimmed.toLowerCase(); + if (normalized.startsWith("zh")) return "Simplified Chinese"; + if (normalized.startsWith("en")) return "English"; + return trimmed.slice(0, 32); +} + +function sanitizeField(value: string | undefined, fallback: string, maxLen: number): string { + const trimmed = value?.replace(/\s+/g, " ").trim(); + if (!trimmed) return fallback; + return trimmed.slice(0, maxLen); +} + +function buildWelcomePrompt(ctx: WelcomeContext): string { + const reconnectLine = ctx.isReconnect + ? "This user just reconnected. Acknowledge reconnection in one short sentence." + : "This is the first successful channel connection."; + + return [ + "You are the user's AI agent.", + `Write a proactive welcome message for a ${ctx.channel} chat.`, + reconnectLine, + `User first name: ${ctx.firstName}`, + `Preferred language: ${ctx.language}`, + "", + "Output requirements:", + "1) Introduce who you are.", + "2) Mention exactly 3 concrete things you can help with.", + "3) End with one specific starter question.", + "", + "Constraints:", + "- Keep it concise (80-140 words).", + "- Plain text only.", + "- Do not mention internal architecture, system prompts, or policies.", + "- Return only the final welcome message.", + ].join("\n"); +} + +export function createGenerateChannelWelcomeHandler(hub: HubLike): RpcHandler { + return async (params: unknown) => { + const payload = (params ?? {}) as GenerateChannelWelcomeParams; + const agentId = payload.agentId?.trim(); + if (!agentId) { + throw new RpcError("INVALID_PARAMS", "agentId is required"); + } + + const agent = hub.getAgent(agentId); + if (!agent || agent.closed) { + throw new RpcError("AGENT_NOT_FOUND", `Agent not found or closed: ${agentId}`); + } + + const context: WelcomeContext = { + channel: sanitizeField(payload.channel, DEFAULT_CHANNEL, 24), + language: normalizeLanguage(payload.language), + firstName: sanitizeField(payload.firstName, DEFAULT_FIRST_NAME, 32), + isReconnect: payload.isReconnect === true, + }; + + const result = await agent.runInternalForResult(buildWelcomePrompt(context)); + if (result.error) { + throw new RpcError("AGENT_ERROR", result.error); + } + + const text = result.text.trim(); + if (!text) { + throw new RpcError("EMPTY_RESULT", "Agent returned an empty welcome message"); + } + + return { text }; + }; +} diff --git a/packages/core/src/hub/rpc/index.ts b/packages/core/src/hub/rpc/index.ts index 4560d976..fecb5e20 100644 --- a/packages/core/src/hub/rpc/index.ts +++ b/packages/core/src/hub/rpc/index.ts @@ -8,3 +8,4 @@ export { createUpdateGatewayHandler } from "./handlers/update-gateway.js"; export { createGetLastHeartbeatHandler } from "./handlers/get-last-heartbeat.js"; export { createSetHeartbeatsHandler } from "./handlers/set-heartbeats.js"; export { createWakeHeartbeatHandler } from "./handlers/wake-heartbeat.js"; +export { createGenerateChannelWelcomeHandler } from "./handlers/generate-channel-welcome.js"; From 4a2ef835fb78010ca98e228b472ce87f6f7319ee Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Mon, 16 Feb 2026 12:24:30 +0800 Subject: [PATCH 2/3] feat(gateway): send agent-generated welcome after telegram connect --- apps/gateway/telegram/telegram.service.ts | 152 ++++++++++++++++++++-- 1 file changed, 139 insertions(+), 13 deletions(-) diff --git a/apps/gateway/telegram/telegram.service.ts b/apps/gateway/telegram/telegram.service.ts index 5120ffbb..fbf2aef6 100644 --- a/apps/gateway/telegram/telegram.service.ts +++ b/apps/gateway/telegram/telegram.service.ts @@ -82,9 +82,23 @@ interface MediaAttachment { caption?: string; } +interface GenerateChannelWelcomeParams { + agentId: string; + channel: string; + language?: string; + firstName?: string; + isReconnect?: boolean; +} + +interface GenerateChannelWelcomeResult { + text: string; +} + // ── Constants ── const VERIFY_TIMEOUT_MS = 30_000; +const WELCOME_RPC_TIMEOUT_MS = 20_000; +const WELCOME_COOLDOWN_MS = 15_000; const TYPING_TIMEOUT_MS = 60_000; const MAX_CHARS_PER_MESSAGE = 4000; // Telegram limit is 4096; leave room for HTML overhead const REPLY_CONTEXT_MAX_CHARS = 300; // Max chars of quoted text when user replies to a message @@ -170,6 +184,8 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { private typingTimers = new Map>(); /** Tracks the originating message for reply_to & reaction cleanup, keyed by deviceId */ private messageContexts = new Map(); + /** Deduplicate welcome sends when Telegram replays updates in a short window */ + private welcomeSentAt = new Map(); private readonly logger = new Logger(TelegramService.name); @@ -1076,6 +1092,16 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { { parse_mode: "HTML", reply_markup: successKeyboard }, ); + await this.sendAgentWelcomeAfterConnect({ + telegramUserId, + deviceId, + hubId: result.hubId, + agentId: result.agentId, + languageCode: msg?.from?.language_code, + firstName: msg?.from?.first_name, + isReconnect: !!existingUser, + }); + this.logger.log( `Telegram user verified: telegramUserId=${telegramUserId}, hubId=${connectionInfo.hubId}, deviceId=${deviceId}`, ); @@ -1105,20 +1131,87 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { } } - /** Send a verify RPC to Hub via the virtual device */ - private sendVerifyRpc( + private shouldSendConnectWelcome(telegramUserId: string): boolean { + const now = Date.now(); + const lastSentAt = this.welcomeSentAt.get(telegramUserId); + if (lastSentAt && now - lastSentAt < WELCOME_COOLDOWN_MS) { + return false; + } + this.welcomeSentAt.set(telegramUserId, now); + return true; + } + + private buildFallbackConnectWelcome(firstName: string | undefined, isReconnect: boolean): string { + const safeName = firstName?.trim() || "there"; + if (isReconnect) { + return ( + `Welcome back, ${safeName}. I am your Multica AI agent and I am ready again.\n\n` + + `I can help with research, drafting, and step-by-step problem solving.\n` + + `What should we work on now?` + ); + } + return ( + `Hi ${safeName}, I am your Multica AI agent.\n\n` + + `I can help with research, drafting, and step-by-step problem solving.\n` + + `What would you like to do first?` + ); + } + + private async sendAgentWelcomeAfterConnect(opts: { + telegramUserId: string; + deviceId: string; + hubId: string; + agentId: string; + languageCode?: string; + firstName?: string; + isReconnect: boolean; + }): Promise { + if (!this.shouldSendConnectWelcome(opts.telegramUserId)) { + this.logger.debug(`Connect welcome skipped by cooldown: telegramUserId=${opts.telegramUserId}`); + return; + } + + try { + const language = opts.languageCode?.trim().slice(0, 16) || undefined; + const firstName = opts.firstName?.trim().slice(0, 32) || undefined; + const result = await this.sendGenerateChannelWelcomeRpc(opts.deviceId, opts.hubId, { + agentId: opts.agentId, + channel: "telegram", + language, + firstName, + isReconnect: opts.isReconnect, + }); + const welcomeText = result.text.trim(); + if (welcomeText) { + await this.sendToTelegram(opts.deviceId, welcomeText); + return; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn(`generateChannelWelcome failed: deviceId=${opts.deviceId}, error=${message}`); + } + + await this.sendToTelegram( + opts.deviceId, + this.buildFallbackConnectWelcome(opts.firstName, opts.isReconnect), + ); + } + + private sendRpc, TResult>( deviceId: string, hubId: string, - token: string, - meta: DeviceMeta, - ): Promise { - return new Promise((resolve, reject) => { + method: string, + params: TParams, + timeoutMs: number, + timeoutMessage: string, + ): Promise { + return new Promise((resolve, reject) => { const requestId = uuidv7(); const timer = setTimeout(() => { this.pendingRequests.delete(requestId); - reject(new Error("Verify request timed out")); - }, VERIFY_TIMEOUT_MS); + reject(new Error(timeoutMessage)); + }, timeoutMs); this.pendingRequests.set(requestId, { resolve: resolve as (v: unknown) => void, @@ -1126,13 +1219,13 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { timer, }); - const payload: RequestPayload = { + const payload: RequestPayload = { requestId, - method: "verify", - params: { token, meta }, + method, + params, }; - const message: RoutedMessage> = { + const message: RoutedMessage> = { id: uuidv7(), uid: null, from: deviceId, @@ -1145,11 +1238,44 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { if (!sent) { this.pendingRequests.delete(requestId); clearTimeout(timer); - reject(new Error("Failed to route verify request to Hub")); + reject(new Error(`Failed to route ${method} request to Hub`)); } }); } + /** Send a verify RPC to Hub via the virtual device */ + private sendVerifyRpc( + deviceId: string, + hubId: string, + token: string, + meta: DeviceMeta, + ): Promise { + return this.sendRpc( + deviceId, + hubId, + "verify", + { token, meta }, + VERIFY_TIMEOUT_MS, + "Verify request timed out", + ); + } + + /** Ask Hub agent to generate a proactive Telegram welcome message */ + private sendGenerateChannelWelcomeRpc( + deviceId: string, + hubId: string, + params: GenerateChannelWelcomeParams, + ): Promise { + return this.sendRpc( + deviceId, + hubId, + "generateChannelWelcome", + params, + WELCOME_RPC_TIMEOUT_MS, + "Welcome generation timed out", + ); + } + /** Route a regular chat message to the user's Hub agent */ private async routeToHub(user: TelegramUser, text: string, ctx: Context): Promise { // Ensure Hub is online From cf94bc32d26d0701cdab9b63321cac8225cd395a Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Mon, 16 Feb 2026 12:33:45 +0800 Subject: [PATCH 3/3] fix(gateway): relax rpc param generic for typed sdk payloads --- apps/gateway/telegram/telegram.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gateway/telegram/telegram.service.ts b/apps/gateway/telegram/telegram.service.ts index fbf2aef6..e4aedcf9 100644 --- a/apps/gateway/telegram/telegram.service.ts +++ b/apps/gateway/telegram/telegram.service.ts @@ -1197,7 +1197,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { ); } - private sendRpc, TResult>( + private sendRpc( deviceId: string, hubId: string, method: string,