From 24b93975ac83552e56835bd9bea3b912554d7ff3 Mon Sep 17 00:00:00 2001 From: Saul Carlin Date: Mon, 9 Feb 2026 23:46:05 -0800 Subject: [PATCH] feat: send generated greeting when user joins via invite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After accepting a join request (onInvite) or joining a conversation (/convos/join), trigger the normal reply pipeline with a system context message so the agent generates a contextual introduction based on its instructions. No new endpoints needed — greetings fire automatically from the existing invite-accept and join handlers. Co-Authored-By: Claude Opus 4.6 --- extensions/convos/index.ts | 23 +++++++++++++--- extensions/convos/src/channel.ts | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/extensions/convos/index.ts b/extensions/convos/index.ts index e822bef521597..eb678f360e66d 100644 --- a/extensions/convos/index.ts +++ b/extensions/convos/index.ts @@ -5,7 +5,7 @@ import * as path from "node:path"; import { emptyPluginConfigSchema, renderQrPngBase64 } from "openclaw/plugin-sdk"; import type { ConvosSDKClient } from "./src/sdk-client.js"; import { resolveConvosAccount, type CoreConfig } from "./src/accounts.js"; -import { convosPlugin } from "./src/channel.js"; +import { convosPlugin, triggerGreeting } from "./src/channel.js"; import { getClientForAccount } from "./src/outbound.js"; import { getConvosRuntime, setConvosRuntime, setConvosSetupActive } from "./src/runtime.js"; import { resolveConvosDbPath } from "./src/sdk-client.js"; @@ -446,6 +446,21 @@ const plugin = { } const result = await client.joinConversation(inviteUrl, name); + + // Trigger a generated greeting in the background after joining. + if (result.status === "joined") { + triggerGreeting({ + account, + conversationId: result.conversationId, + senderId: "system", + context: + "[System] You've just been added to a user's pre-existing chat. Introduce yourself concisely.", + runtime, + }).catch((err) => { + console.error("[convos-join] Greeting generation failed:", err); + }); + } + jsonResponse(res, 200, { conversationId: result.conversationId, status: result.status, @@ -467,7 +482,8 @@ const plugin = { } try { const body = await readJsonBody(req); - const conversationId = typeof body.conversationId === "string" ? body.conversationId : undefined; + const conversationId = + typeof body.conversationId === "string" ? body.conversationId : undefined; const message = typeof body.message === "string" ? body.message : undefined; if (!conversationId || !message) { jsonResponse(res, 400, { error: "conversationId and message (strings) are required" }); @@ -505,7 +521,8 @@ const plugin = { } try { const body = await readJsonBody(req); - const conversationId = typeof body.conversationId === "string" ? body.conversationId : undefined; + const conversationId = + typeof body.conversationId === "string" ? body.conversationId : undefined; const name = typeof body.name === "string" ? body.name : undefined; if (!conversationId) { diff --git a/extensions/convos/src/channel.ts b/extensions/convos/src/channel.ts index 68314bec90cd0..6e26576d41c65 100644 --- a/extensions/convos/src/channel.ts +++ b/extensions/convos/src/channel.ts @@ -277,6 +277,10 @@ export const convosPlugin: ChannelPlugin = { `[${account.accountId}] XMTP stateDir: ${stateDir}, cwd: ${process.cwd()}, dbPath: ${dbPath}`, ); + // Track conversations that have already received a greeting so we + // only greet the first user who joins, not every subsequent invite. + const greetedConversations = new Set(); + // Create SDK client with message handling const client = await ConvosSDKClient.create({ privateKey: account.privateKey, @@ -294,6 +298,22 @@ export const convosPlugin: ChannelPlugin = { // TODO: Add policy-based handling log?.info(`[${account.accountId}] Auto-accepting invite request`); await inviteCtx.accept(); + + // Greet only the first user who joins each conversation. + if (!greetedConversations.has(inviteCtx.conversationId)) { + greetedConversations.add(inviteCtx.conversationId); + triggerGreeting({ + account, + conversationId: inviteCtx.conversationId, + senderId: inviteCtx.joinerInboxId, + context: + "[System] You've just created this group and a user has just joined. Introduce yourself concisely.", + runtime, + log, + }).catch((err) => { + log?.error(`[${account.accountId}] Greeting generation failed: ${String(err)}`); + }); + } }, }); @@ -508,6 +528,31 @@ async function deliverConvosReply(params: { } } +/** + * Trigger a generated greeting by feeding a synthetic system message through + * the normal reply pipeline. The agent will produce a contextual introduction + * based on its instructions / system prompt. + */ +export async function triggerGreeting(params: { + account: ResolvedConvosAccount; + conversationId: string; + senderId: string; + context: string; + runtime: PluginRuntime; + log?: RuntimeLogger; +}): Promise { + const { account, conversationId, senderId, context, runtime, log } = params; + const syntheticMsg: InboundMessage = { + conversationId, + messageId: `greeting-${Date.now()}`, + senderId, + senderName: "", + content: context, + timestamp: new Date(), + }; + await handleInboundMessage(account, syntheticMsg, runtime, log); +} + /** * Stop SDK client for an account */