fix: normalize prefix wiring and types (#9001) (thanks @mudrii)

This commit is contained in:
Gustavo Madeira Santana
2026-02-04 16:07:23 -05:00
parent f321732dfe
commit 3369175954
28 changed files with 255 additions and 218 deletions

View File

@@ -1,7 +1,7 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import {
createReplyPrefixContext,
createReplyPrefixOptions,
logAckFailure,
logInboundDrop,
logTypingFailure,
@@ -2174,7 +2174,7 @@ async function processMessage(
}, typingRestartDelayMs);
};
try {
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: config,
agentId: route.agentId,
channel: "bluebubbles",
@@ -2184,8 +2184,7 @@ async function processMessage(
ctx: ctxPayload,
cfg: config,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload, info) => {
const rawReplyToId =
typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
@@ -2297,7 +2296,7 @@ async function processMessage(
},
},
replyOptions: {
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
disableBlockStreaming:
typeof account.config.blockStreaming === "boolean"
? !account.config.blockStreaming

View File

@@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import { createReplyPrefixContext, resolveMentionGatingWithBypass } from "openclaw/plugin-sdk";
import { createReplyPrefixOptions, resolveMentionGatingWithBypass } from "openclaw/plugin-sdk";
import type {
GoogleChatAnnotation,
GoogleChatAttachment,
@@ -725,7 +725,7 @@ async function processMessageWithPipeline(params: {
}
}
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: config,
agentId: route.agentId,
channel: "googlechat",
@@ -736,8 +736,7 @@ async function processMessageWithPipeline(params: {
ctx: ctxPayload,
cfg: config,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload) => {
await deliverGoogleChatReply({
payload,
@@ -759,7 +758,7 @@ async function processMessageWithPipeline(params: {
},
},
replyOptions: {
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
}

View File

@@ -1,6 +1,6 @@
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
import {
createReplyPrefixContext,
createReplyPrefixOptions,
createTypingCallbacks,
formatAllowlistMatchMeta,
logInboundDrop,
@@ -579,7 +579,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
channel: "matrix",
accountId: route.accountId,
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "matrix",
@@ -609,8 +609,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
});
const { dispatcher, replyOptions, markDispatchIdle } =
core.channel.reply.createReplyDispatcherWithTyping({
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
await deliverMatrixReplies({
@@ -640,7 +639,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
replyOptions: {
...replyOptions,
skillFilter: roomConfig?.skills,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
markDispatchIdle();

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
import { createReplyPrefixOptions } from "openclaw/plugin-sdk";
import { describe, expect, it } from "vitest";
import { mattermostPlugin } from "./channel.js";
describe("mattermostPlugin", () => {
@@ -59,7 +59,7 @@ describe("mattermostPlugin", () => {
},
};
const prefixContext = createReplyPrefixContext({
const prefixContext = createReplyPrefixOptions({
cfg,
agentId: "main",
channel: "mattermost",

View File

@@ -5,7 +5,7 @@ import type {
RuntimeEnv,
} from "openclaw/plugin-sdk";
import {
createReplyPrefixContext,
createReplyPrefixOptions,
createTypingCallbacks,
logInboundDrop,
logTypingFailure,
@@ -760,7 +760,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
accountId: account.accountId,
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "mattermost",
@@ -780,8 +780,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
});
const { dispatcher, replyOptions, markDispatchIdle } =
core.channel.reply.createReplyDispatcherWithTyping({
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload: ReplyPayload) => {
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
@@ -830,7 +829,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
...replyOptions,
disableBlockStreaming:
typeof account.blockStreaming === "boolean" ? !account.blockStreaming : undefined,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
markDispatchIdle();

View File

@@ -1,5 +1,5 @@
import {
createReplyPrefixContext,
createReplyPrefixOptions,
createTypingCallbacks,
logTypingFailure,
resolveChannelMediaMaxBytes,
@@ -56,7 +56,7 @@ export function createMSTeamsReplyDispatcher(params: {
});
},
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: params.cfg,
agentId: params.agentId,
channel: "msteams",
@@ -66,8 +66,7 @@ export function createMSTeamsReplyDispatcher(params: {
const { dispatcher, replyOptions, markDispatchIdle } =
core.channel.reply.createReplyDispatcherWithTyping({
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
deliver: async (payload) => {
const tableMode = core.channel.text.resolveMarkdownTableMode({
@@ -127,7 +126,7 @@ export function createMSTeamsReplyDispatcher(params: {
return {
dispatcher,
replyOptions: { ...replyOptions, onModelSelected: prefixContext.onModelSelected },
replyOptions: { ...replyOptions, onModelSelected },
markDispatchIdle,
};
}

View File

@@ -1,5 +1,5 @@
import {
createReplyPrefixContext,
createReplyPrefixOptions,
logInboundDrop,
resolveControlCommandGate,
type OpenClawConfig,
@@ -286,7 +286,7 @@ export async function handleNextcloudTalkInbound(params: {
},
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: config as OpenClawConfig,
agentId: route.agentId,
channel: CHANNEL_ID,
@@ -297,8 +297,7 @@ export async function handleNextcloudTalkInbound(params: {
ctx: ctxPayload,
cfg: config as OpenClawConfig,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload) => {
await deliverNextcloudTalkReply({
payload: payload as {
@@ -318,7 +317,7 @@ export async function handleNextcloudTalkInbound(params: {
},
replyOptions: {
skillFilter: roomConfig?.skills,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
disableBlockStreaming:
typeof account.config.blockStreaming === "boolean"
? !account.config.blockStreaming

View File

@@ -1,6 +1,6 @@
import type { RuntimeEnv, ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk";
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
import { format } from "node:util";
import { createReplyPrefixOptions } from "openclaw/plugin-sdk";
import { getTlonRuntime } from "../runtime.js";
import { normalizeShip, parseChannelNest } from "../targets.js";
import { resolveTlonAccount } from "../types.js";
@@ -29,6 +29,29 @@ type ChannelAuthorization = {
allowedShips?: string[];
};
type UrbitMemo = {
author?: string;
content?: unknown;
sent?: number;
};
type UrbitUpdate = {
id?: string | number;
response?: {
add?: { memo?: UrbitMemo };
post?: {
id?: string | number;
"r-post"?: {
set?: { essay?: UrbitMemo };
reply?: {
id?: string | number;
"r-reply"?: { set?: { memo?: UrbitMemo } };
};
};
};
};
};
function resolveChannelAuthorization(
cfg: OpenClawConfig,
channelNest: string,
@@ -121,15 +144,14 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
runtime.log?.("[tlon] No group channels to monitor (DMs only)");
}
// oxlint-disable-next-line typescript/no-explicit-any
const handleIncomingDM = async (update: any) => {
const handleIncomingDM = async (update: UrbitUpdate) => {
try {
const memo = update?.response?.add?.memo;
if (!memo) {
return;
}
const messageId = update.id as string | undefined;
const messageId = update.id != null ? String(update.id) : undefined;
if (!processedTracker.mark(messageId)) {
return;
}
@@ -161,25 +183,24 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
}
};
// oxlint-disable-next-line typescript/no-explicit-any
const handleIncomingGroupMessage = (channelNest: string) => async (update: any) => {
const handleIncomingGroupMessage = (channelNest: string) => async (update: UrbitUpdate) => {
try {
const parsed = parseChannelNest(channelNest);
if (!parsed) {
return;
}
const essay = update?.response?.post?.["r-post"]?.set?.essay;
const memo = update?.response?.post?.["r-post"]?.reply?.["r-reply"]?.set?.memo;
const post = update?.response?.post?.["r-post"];
const essay = post?.set?.essay;
const memo = post?.reply?.["r-reply"]?.set?.memo;
if (!essay && !memo) {
return;
}
const content = memo || essay;
const isThreadReply = Boolean(memo);
const messageId = isThreadReply
? update?.response?.post?.["r-post"]?.reply?.id
: update?.response?.post?.id;
const rawMessageId = isThreadReply ? post?.reply?.id : update?.response?.post?.id;
const messageId = rawMessageId != null ? String(rawMessageId) : undefined;
if (!processedTracker.mark(messageId)) {
return;
@@ -356,7 +377,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
const dispatchStartTime = Date.now();
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "tlon",
@@ -368,8 +389,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay,
deliver: async (payload: ReplyPayload) => {
let replyText = payload.text;
@@ -413,7 +433,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
},
},
replyOptions: {
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
};

View File

@@ -6,7 +6,7 @@
*/
import type { ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk";
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
import { createReplyPrefixOptions } from "openclaw/plugin-sdk";
import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
import { checkTwitchAccessControl } from "./access-control.js";
import { getOrCreateClientManager } from "./client-manager-registry.js";
@@ -104,7 +104,7 @@ async function processTwitchMessage(params: {
channel: "twitch",
accountId,
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "twitch",
@@ -115,8 +115,7 @@ async function processTwitchMessage(params: {
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload) => {
await deliverTwitchReply({
payload,
@@ -131,7 +130,7 @@ async function processTwitchMessage(params: {
},
},
replyOptions: {
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
}

View File

@@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig, MarkdownTableMode } from "openclaw/plugin-sdk";
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
import { createReplyPrefixOptions } from "openclaw/plugin-sdk";
import type { ResolvedZaloAccount } from "./accounts.js";
import {
ZaloApiError,
@@ -584,7 +584,7 @@ async function processMessageWithPipeline(params: {
channel: "zalo",
accountId: account.accountId,
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: config,
agentId: route.agentId,
channel: "zalo",
@@ -595,8 +595,7 @@ async function processMessageWithPipeline(params: {
ctx: ctxPayload,
cfg: config,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload) => {
await deliverZaloReply({
payload,
@@ -616,7 +615,7 @@ async function processMessageWithPipeline(params: {
},
},
replyOptions: {
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
}

View File

@@ -1,6 +1,6 @@
import type { ChildProcess } from "node:child_process";
import type { OpenClawConfig, MarkdownTableMode, RuntimeEnv } from "openclaw/plugin-sdk";
import { createReplyPrefixContext, mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk";
import { createReplyPrefixOptions, mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk";
import type { ResolvedZalouserAccount, ZcaFriend, ZcaGroup, ZcaMessage } from "./types.js";
import { getZalouserRuntime } from "./runtime.js";
import { sendMessageZalouser } from "./send.js";
@@ -334,7 +334,7 @@ async function processMessage(
},
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: config,
agentId: route.agentId,
channel: "zalouser",
@@ -345,8 +345,7 @@ async function processMessage(
ctx: ctxPayload,
cfg: config,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload) => {
await deliverZalouserReply({
payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
@@ -370,7 +369,7 @@ async function processMessage(
},
},
replyOptions: {
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
}

View File

@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolveResponsePrefix, resolveEffectiveMessagesConfig } from "./identity.js";
const makeConfig = <T extends OpenClawConfig>(cfg: T) => cfg;
describe("resolveResponsePrefix with per-channel override", () => {
// ─── Backward compatibility ─────────────────────────────────────────
@@ -36,66 +38,66 @@ describe("resolveResponsePrefix with per-channel override", () => {
describe("channel-level prefix", () => {
it("returns channel prefix when set, ignoring global", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
whatsapp: { responsePrefix: "[WA] " } as any,
whatsapp: { responsePrefix: "[WA] " },
},
};
} satisfies OpenClawConfig);
expect(resolveResponsePrefix(cfg, "main", { channel: "whatsapp" })).toBe("[WA] ");
});
it("falls through to global when channel prefix is undefined", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
whatsapp: {} as any,
whatsapp: {},
},
};
} satisfies OpenClawConfig);
expect(resolveResponsePrefix(cfg, "main", { channel: "whatsapp" })).toBe("[Global] ");
});
it("channel empty string stops cascade (no global prefix applied)", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
telegram: { responsePrefix: "" } as any,
telegram: { responsePrefix: "" },
},
};
} satisfies OpenClawConfig);
expect(resolveResponsePrefix(cfg, "main", { channel: "telegram" })).toBe("");
});
it("resolves 'auto' at channel level to identity name", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
agents: {
list: [{ id: "main", identity: { name: "MyBot" } }],
},
channels: {
whatsapp: { responsePrefix: "auto" } as any,
whatsapp: { responsePrefix: "auto" },
},
};
} satisfies OpenClawConfig);
expect(resolveResponsePrefix(cfg, "main", { channel: "whatsapp" })).toBe("[MyBot]");
});
it("different channels get different prefixes", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
channels: {
whatsapp: { responsePrefix: "[WA Bot] " } as any,
telegram: { responsePrefix: "" } as any,
discord: { responsePrefix: "🤖 " } as any,
whatsapp: { responsePrefix: "[WA Bot] " },
telegram: { responsePrefix: "" },
discord: { responsePrefix: "🤖 " },
},
};
} satisfies OpenClawConfig);
expect(resolveResponsePrefix(cfg, "main", { channel: "whatsapp" })).toBe("[WA Bot] ");
expect(resolveResponsePrefix(cfg, "main", { channel: "telegram" })).toBe("");
expect(resolveResponsePrefix(cfg, "main", { channel: "discord" })).toBe("🤖 ");
});
it("returns undefined when channel not in config", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
channels: {
whatsapp: { responsePrefix: "[WA] " } as any,
whatsapp: { responsePrefix: "[WA] " },
},
};
} satisfies OpenClawConfig);
expect(resolveResponsePrefix(cfg, "main", { channel: "telegram" })).toBeUndefined();
});
});
@@ -104,7 +106,7 @@ describe("resolveResponsePrefix with per-channel override", () => {
describe("account-level prefix", () => {
it("returns account prefix when set, ignoring channel and global", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
whatsapp: {
@@ -112,48 +114,48 @@ describe("resolveResponsePrefix with per-channel override", () => {
accounts: {
business: { responsePrefix: "[Biz] " },
},
} as any,
},
},
};
} satisfies OpenClawConfig);
expect(
resolveResponsePrefix(cfg, "main", { channel: "whatsapp", accountId: "business" }),
).toBe("[Biz] ");
});
it("falls through to channel prefix when account prefix is undefined", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
channels: {
whatsapp: {
responsePrefix: "[WA] ",
accounts: {
business: {},
},
} as any,
},
},
};
} satisfies OpenClawConfig);
expect(
resolveResponsePrefix(cfg, "main", { channel: "whatsapp", accountId: "business" }),
).toBe("[WA] ");
});
it("falls through to global when both account and channel are undefined", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
whatsapp: {
accounts: {
business: {},
},
} as any,
},
},
};
} satisfies OpenClawConfig);
expect(
resolveResponsePrefix(cfg, "main", { channel: "whatsapp", accountId: "business" }),
).toBe("[Global] ");
});
it("account empty string stops cascade", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
whatsapp: {
@@ -161,16 +163,16 @@ describe("resolveResponsePrefix with per-channel override", () => {
accounts: {
business: { responsePrefix: "" },
},
} as any,
},
},
};
} satisfies OpenClawConfig);
expect(
resolveResponsePrefix(cfg, "main", { channel: "whatsapp", accountId: "business" }),
).toBe("");
});
it("resolves 'auto' at account level to identity name", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
agents: {
list: [{ id: "main", identity: { name: "BizBot" } }],
},
@@ -179,16 +181,16 @@ describe("resolveResponsePrefix with per-channel override", () => {
accounts: {
business: { responsePrefix: "auto" },
},
} as any,
},
},
};
} satisfies OpenClawConfig);
expect(
resolveResponsePrefix(cfg, "main", { channel: "whatsapp", accountId: "business" }),
).toBe("[BizBot]");
});
it("different accounts on same channel get different prefixes", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
channels: {
whatsapp: {
responsePrefix: "[WA] ",
@@ -196,9 +198,9 @@ describe("resolveResponsePrefix with per-channel override", () => {
business: { responsePrefix: "[Biz] " },
personal: { responsePrefix: "[Personal] " },
},
} as any,
},
},
};
} satisfies OpenClawConfig);
expect(
resolveResponsePrefix(cfg, "main", { channel: "whatsapp", accountId: "business" }),
).toBe("[Biz] ");
@@ -208,16 +210,16 @@ describe("resolveResponsePrefix with per-channel override", () => {
});
it("unknown accountId falls through to channel level", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
channels: {
whatsapp: {
responsePrefix: "[WA] ",
accounts: {
business: { responsePrefix: "[Biz] " },
},
} as any,
},
},
};
} satisfies OpenClawConfig);
expect(
resolveResponsePrefix(cfg, "main", { channel: "whatsapp", accountId: "unknown" }),
).toBe("[WA] ");
@@ -227,7 +229,7 @@ describe("resolveResponsePrefix with per-channel override", () => {
// ─── Full cascade ─────────────────────────────────────────────────
describe("full 4-level cascade", () => {
const fullCfg: OpenClawConfig = {
const fullCfg = makeConfig({
agents: {
list: [{ id: "main", identity: { name: "TestBot" } }],
},
@@ -239,10 +241,10 @@ describe("resolveResponsePrefix with per-channel override", () => {
business: { responsePrefix: "[L1-Account] " },
default: {},
},
} as any,
telegram: {} as any,
},
telegram: {},
},
};
} satisfies OpenClawConfig);
it("L1: account prefix wins when all levels set", () => {
expect(
@@ -261,9 +263,9 @@ describe("resolveResponsePrefix with per-channel override", () => {
});
it("undefined: no prefix at any level", () => {
const cfg: OpenClawConfig = {
channels: { telegram: {} as any },
};
const cfg = makeConfig({
channels: { telegram: {} },
} satisfies OpenClawConfig);
expect(resolveResponsePrefix(cfg, "main", { channel: "telegram" })).toBeUndefined();
});
});
@@ -272,12 +274,12 @@ describe("resolveResponsePrefix with per-channel override", () => {
describe("resolveEffectiveMessagesConfig with channel context", () => {
it("passes channel context through to responsePrefix resolution", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
whatsapp: { responsePrefix: "[WA] " } as any,
whatsapp: { responsePrefix: "[WA] " },
},
};
} satisfies OpenClawConfig);
const result = resolveEffectiveMessagesConfig(cfg, "main", {
channel: "whatsapp",
});
@@ -285,12 +287,12 @@ describe("resolveResponsePrefix with per-channel override", () => {
});
it("uses global when no channel context provided", () => {
const cfg: OpenClawConfig = {
const cfg = makeConfig({
messages: { responsePrefix: "[Global] " },
channels: {
whatsapp: { responsePrefix: "[WA] " } as any,
whatsapp: { responsePrefix: "[WA] " },
},
};
} satisfies OpenClawConfig);
const result = resolveEffectiveMessagesConfig(cfg, "main");
expect(result.responsePrefix).toBe("[Global] ");
});

View File

@@ -15,6 +15,11 @@ export type ReplyPrefixContextBundle = {
onModelSelected: (ctx: ModelSelectionContext) => void;
};
export type ReplyPrefixOptions = Pick<
ReplyPrefixContextBundle,
"responsePrefix" | "responsePrefixContextProvider" | "onModelSelected"
>;
export function createReplyPrefixContext(params: {
cfg: OpenClawConfig;
agentId: string;
@@ -44,3 +49,14 @@ export function createReplyPrefixContext(params: {
onModelSelected,
};
}
export function createReplyPrefixOptions(params: {
cfg: OpenClawConfig;
agentId: string;
channel?: string;
accountId?: string;
}): ReplyPrefixOptions {
const { responsePrefix, responsePrefixContextProvider, onModelSelected } =
createReplyPrefixContext(params);
return { responsePrefix, responsePrefixContextProvider, onModelSelected };
}

View File

@@ -20,7 +20,7 @@ import {
shouldAckReaction as shouldAckReactionGate,
} from "../../channels/ack-reactions.js";
import { logTypingFailure, logAckFailure } from "../../channels/logging.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { recordInboundSession } from "../../channels/session.js";
import { createTypingCallbacks } from "../../channels/typing.js";
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
@@ -334,7 +334,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
? deliverTarget.slice("channel:".length)
: message.channelId;
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "discord",
@@ -347,8 +347,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
});
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload: ReplyPayload) => {
const replyToId = replyReference.use();
@@ -394,9 +393,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
typeof discordConfig?.blockStreaming === "boolean"
? !discordConfig.blockStreaming
: undefined,
onModelSelected: (ctx) => {
prefixContext.onModelSelected(ctx);
},
onModelSelected,
},
});
markDispatchIdle();

View File

@@ -33,7 +33,7 @@ import {
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import {
readChannelAllowFromStore,
@@ -791,7 +791,7 @@ async function dispatchDiscordCommandInteraction(params: {
CommandSource: "native" as const,
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "discord",
@@ -803,8 +803,7 @@ async function dispatchDiscordCommandInteraction(params: {
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
try {
@@ -837,7 +836,7 @@ async function dispatchDiscordCommandInteraction(params: {
typeof discordConfig?.blockStreaming === "boolean"
? !discordConfig.blockStreaming
: undefined,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
}

View File

@@ -1,8 +1,8 @@
import type { Client } from "@larksuiteoapi/node-sdk";
import type { OpenClawConfig } from "../config/config.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { resolveSessionAgentId } from "../agents/agent-scope.js";
import { createReplyPrefixContext } from "../channels/reply-prefix.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
import { loadConfig } from "../config/config.js";
import { logVerbose } from "../globals.js";
import { formatErrorMessage } from "../infra/errors.js";
@@ -304,8 +304,8 @@ export async function processFeishuMessage(
WasMentioned: isGroup ? wasMentioned : undefined,
};
const agentId = resolveSessionAgentId({ sessionKey: ctx.SessionKey, config: cfg });
const prefixContext = createReplyPrefixContext({
const agentId = resolveSessionAgentId({ config: cfg });
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId,
channel: "feishu",
@@ -316,8 +316,7 @@ export async function processFeishuMessage(
ctx,
cfg,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload, info) => {
const hasMedia = payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0);
if (!payload.text && !hasMedia) {
@@ -403,7 +402,7 @@ export async function processFeishuMessage(
},
replyOptions: {
disableBlockStreaming: !feishuCfg.blockStreaming,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
onPartialReply: streamingSession
? async (payload) => {
if (!streamingSession.isActive() || !payload.text) {

View File

@@ -5,15 +5,11 @@ import path from "node:path";
import type { MsgContext } from "../../auto-reply/templating.js";
import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../agents/identity.js";
import { resolveThinkingDefault } from "../../agents/model-selection.js";
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../../auto-reply/reply/response-prefix-template.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import {
@@ -477,15 +473,14 @@ export const chatHandlers: GatewayRequestHandlers = {
sessionKey: p.sessionKey,
config: cfg,
});
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, agentId),
};
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId,
channel: INTERNAL_MESSAGE_CHANNEL,
});
const finalReplyParts: string[] = [];
const dispatcher = createReplyDispatcher({
responsePrefix: resolveEffectiveMessagesConfig(cfg, agentId, {
channel: INTERNAL_MESSAGE_CHANNEL,
}).responsePrefix,
responsePrefixContextProvider: () => prefixContext,
...prefixOptions,
onError: (err) => {
context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`);
},
@@ -514,12 +509,7 @@ export const chatHandlers: GatewayRequestHandlers = {
onAgentRunStart: () => {
agentRunStarted = true;
},
onModelSelected: (ctx) => {
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
},
onModelSelected,
},
})
.then(() => {

View File

@@ -25,7 +25,7 @@ import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/re
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
import { resolveControlCommandGate } from "../../channels/command-gating.js";
import { logInboundDrop } from "../../channels/logging.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { recordInboundSession } from "../../channels/session.js";
import { loadConfig } from "../../config/config.js";
import {
@@ -610,7 +610,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
);
}
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "imessage",
@@ -618,8 +618,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
});
const dispatcher = createReplyDispatcher({
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
await deliverReplies({
@@ -647,7 +646,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
typeof accountInfo.config.blockStreaming === "boolean"
? !accountInfo.config.blockStreaming
: undefined,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
if (!queuedFinal) {

View File

@@ -5,7 +5,7 @@ import type { RuntimeEnv } from "../runtime.js";
import type { LineChannelData, ResolvedLineAccount } from "./types.js";
import { chunkMarkdownText } from "../auto-reply/chunk.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { createReplyPrefixContext } from "../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
import { danger, logVerbose } from "../globals.js";
import { normalizePluginHttpPath } from "../plugins/http-path.js";
import { registerPluginHttpRoute } from "../plugins/http-registry.js";
@@ -192,7 +192,7 @@ export async function monitorLineProvider(
try {
const textLimit = 5000; // LINE max message length
let replyTokenUsed = false; // Track if we've used the one-time reply token
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: config,
agentId: route.agentId,
channel: "line",
@@ -203,8 +203,7 @@ export async function monitorLineProvider(
ctx: ctxPayload,
cfg: config,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload, _info) => {
const lineData = (payload.channelData?.line as LineChannelData | undefined) ?? {};
@@ -257,7 +256,7 @@ export async function monitorLineProvider(
},
},
replyOptions: {
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});

View File

@@ -148,7 +148,7 @@ export {
shouldAckReactionForWhatsApp,
} from "../channels/ack-reactions.js";
export { createTypingCallbacks } from "../channels/typing.js";
export { createReplyPrefixContext } from "../channels/reply-prefix.js";
export { createReplyPrefixContext, createReplyPrefixOptions } from "../channels/reply-prefix.js";
export { logAckFailure, logInboundDrop, logTypingFailure } from "../channels/logging.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
export type { NormalizedLocation } from "../channels/location.js";

View File

@@ -19,7 +19,7 @@ import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.j
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
import { resolveControlCommandGate } from "../../channels/command-gating.js";
import { logInboundDrop, logTypingFailure } from "../../channels/logging.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { recordInboundSession } from "../../channels/session.js";
import { createTypingCallbacks } from "../../channels/typing.js";
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
@@ -171,7 +171,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
}
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: deps.cfg,
agentId: route.agentId,
channel: "signal",
@@ -200,8 +200,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
});
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId),
deliver: async (payload) => {
await deps.deliverReplies({
@@ -229,9 +228,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
...replyOptions,
disableBlockStreaming:
typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : undefined,
onModelSelected: (ctx) => {
prefixContext.onModelSelected(ctx);
},
onModelSelected,
},
});
markDispatchIdle();

View File

@@ -5,7 +5,7 @@ import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js";
import { logAckFailure, logTypingFailure } from "../../../channels/logging.js";
import { createReplyPrefixContext } from "../../../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../../../channels/reply-prefix.js";
import { createTypingCallbacks } from "../../../channels/typing.js";
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
@@ -95,7 +95,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
},
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "slack",
@@ -103,8 +103,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
});
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
const replyThreadTs = replyPlan.nextThreadTs();
@@ -139,9 +138,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
typeof account.config.blockStreaming === "boolean"
? !account.config.blockStreaming
: undefined,
onModelSelected: (ctx) => {
prefixContext.onModelSelected(ctx);
},
onModelSelected,
},
});
markDispatchIdle();

View File

@@ -16,7 +16,7 @@ import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { resolveConversationLabel } from "../../channels/conversation-label.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js";
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
import { danger, logVerbose } from "../../globals.js";
@@ -434,7 +434,7 @@ export function registerSlackMonitorSlashCommands(params: {
OriginatingTo: `user:${command.user_id}`,
});
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "slack",
@@ -445,8 +445,7 @@ export function registerSlackMonitorSlashCommands(params: {
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload) => {
await deliverSlackSlashReplies({
replies: [payload],
@@ -467,7 +466,7 @@ export function registerSlackMonitorSlashCommands(params: {
},
replyOptions: {
skillFilter: channelConfig?.skills,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
if (counts.final + counts.tool + counts.block === 0) {

View File

@@ -695,3 +695,7 @@ export const buildTelegramMessageContext = async ({
accountId: account.accountId,
};
};
export type TelegramMessageContext = NonNullable<
Awaited<ReturnType<typeof buildTelegramMessageContext>>
>;

View File

@@ -1,3 +1,4 @@
import type { Bot } from "grammy";
import { beforeEach, describe, expect, it, vi } from "vitest";
const createTelegramDraftStream = vi.hoisted(() => vi.fn());
@@ -72,16 +73,25 @@ describe("dispatchTelegramMessage draft streaming", () => {
removeAckAfterReply: false,
};
const bot = { api: { sendMessageDraft: vi.fn() } } as unknown as Bot;
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: () => {
throw new Error("exit");
},
};
await dispatchTelegramMessage({
context,
bot: { api: {} },
bot,
cfg: {},
runtime: {},
runtime,
replyToMode: "first",
streamMode: "partial",
textLimit: 4096,
telegramCfg: {},
opts: {},
opts: { token: "token" },
resolveBotTopicsEnabled,
});

View File

@@ -1,5 +1,10 @@
import type { Bot } from "grammy";
import type { OpenClawConfig, ReplyToMode, TelegramAccountConfig } from "../config/types.js";
import type { RuntimeEnv } from "../runtime.js";
import type { TelegramMessageContext } from "./bot-message-context.js";
import type { TelegramBotOptions } from "./bot.js";
import type { TelegramStreamMode, TelegramContext } from "./bot/types.js";
import { resolveAgentDir } from "../agents/agent-scope.js";
// @ts-nocheck
import {
findModelInCatalog,
loadModelCatalog,
@@ -12,9 +17,8 @@ import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { removeAckReactionAfterReply } from "../channels/ack-reactions.js";
import { logAckFailure, logTypingFailure } from "../channels/logging.js";
import { createReplyPrefixContext } from "../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
import { createTypingCallbacks } from "../channels/typing.js";
import { OpenClawConfig } from "../config/config.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { danger, logVerbose } from "../globals.js";
import { deliverReplies } from "./bot/delivery.js";
@@ -38,6 +42,21 @@ async function resolveStickerVisionSupport(cfg: OpenClawConfig, agentId: string)
}
}
type ResolveBotTopicsEnabled = (ctx: TelegramContext) => boolean | Promise<boolean>;
type DispatchTelegramMessageParams = {
context: TelegramMessageContext;
bot: Bot;
cfg: OpenClawConfig;
runtime: RuntimeEnv;
replyToMode: ReplyToMode;
streamMode: TelegramStreamMode;
textLimit: number;
telegramCfg: TelegramAccountConfig;
opts: Pick<TelegramBotOptions, "token">;
resolveBotTopicsEnabled: ResolveBotTopicsEnabled;
};
export const dispatchTelegramMessage = async ({
context,
bot,
@@ -49,8 +68,7 @@ export const dispatchTelegramMessage = async ({
telegramCfg,
opts,
resolveBotTopicsEnabled,
// oxlint-disable-next-line typescript/no-explicit-any
}: any) => {
}: DispatchTelegramMessageParams) => {
const {
ctxPayload,
primaryCtx,
@@ -157,7 +175,7 @@ export const dispatchTelegramMessage = async ({
Boolean(draftStream) ||
(typeof telegramCfg.blockStreaming === "boolean" ? !telegramCfg.blockStreaming : undefined);
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "telegram",
@@ -207,16 +225,20 @@ export const dispatchTelegramMessage = async ({
}
// Cache the description for future encounters
cacheSticker({
fileId: sticker.fileId,
fileUniqueId: sticker.fileUniqueId,
emoji: sticker.emoji,
setName: sticker.setName,
description,
cachedAt: new Date().toISOString(),
receivedFrom: ctxPayload.From,
});
logVerbose(`telegram: cached sticker description for ${sticker.fileUniqueId}`);
if (sticker.fileId) {
cacheSticker({
fileId: sticker.fileId,
fileUniqueId: sticker.fileUniqueId,
emoji: sticker.emoji,
setName: sticker.setName,
description,
cachedAt: new Date().toISOString(),
receivedFrom: ctxPayload.From,
});
logVerbose(`telegram: cached sticker description for ${sticker.fileUniqueId}`);
} else {
logVerbose(`telegram: skipped sticker cache (missing fileId)`);
}
}
}
@@ -233,8 +255,7 @@ export const dispatchTelegramMessage = async ({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload, info) => {
if (info.kind === "final") {
await flushDraft();
@@ -283,9 +304,7 @@ export const dispatchTelegramMessage = async ({
skillFilter,
disableBlockStreaming,
onPartialReply: draftStream ? (payload) => updateDraftFromPartial(payload.text) : undefined,
onModelSelected: (ctx) => {
prefixContext.onModelSelected(ctx);
},
onModelSelected,
},
});
draftStream?.stop();

View File

@@ -23,7 +23,7 @@ import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
import { createReplyPrefixContext } from "../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
import {
@@ -547,7 +547,7 @@ export const registerTelegramNativeCommands = ({
skippedNonSilent: 0,
};
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,
agentId: route.agentId,
channel: "telegram",
@@ -558,8 +558,7 @@ export const registerTelegramNativeCommands = ({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
...prefixOptions,
deliver: async (payload, _info) => {
const result = await deliverReplies({
replies: [payload],
@@ -590,7 +589,7 @@ export const registerTelegramNativeCommands = ({
replyOptions: {
skillFilter,
disableBlockStreaming,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});
if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {

View File

@@ -18,7 +18,7 @@ import {
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js";
import { toLocationContext } from "../../../channels/location.js";
import { createReplyPrefixContext } from "../../../channels/reply-prefix.js";
import { createReplyPrefixOptions } from "../../../channels/reply-prefix.js";
import { resolveMarkdownTableMode } from "../../../config/markdown-tables.js";
import {
readSessionUpdatedAt,
@@ -255,7 +255,7 @@ export async function processMessage(params: {
? await resolveWhatsAppCommandAuthorized({ cfg: params.cfg, msg: params.msg })
: undefined;
const configuredResponsePrefix = params.cfg.messages?.responsePrefix;
const prefixContext = createReplyPrefixContext({
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg: params.cfg,
agentId: params.route.agentId,
channel: "whatsapp",
@@ -266,7 +266,7 @@ export async function processMessage(params: {
Boolean(params.msg.selfE164) &&
normalizeE164(params.msg.from) === normalizeE164(params.msg.selfE164 ?? "");
const responsePrefix =
prefixContext.responsePrefix ??
prefixOptions.responsePrefix ??
(configuredResponsePrefix === undefined && isSelfChat
? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[openclaw]")
: undefined);
@@ -341,8 +341,8 @@ export async function processMessage(params: {
cfg: params.cfg,
replyResolver: params.replyResolver,
dispatcherOptions: {
...prefixOptions,
responsePrefix,
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
onHeartbeatStrip: () => {
if (!didLogHeartbeatStrip) {
didLogHeartbeatStrip = true;
@@ -402,7 +402,7 @@ export async function processMessage(params: {
typeof params.cfg.channels?.whatsapp?.blockStreaming === "boolean"
? !params.cfg.channels.whatsapp.blockStreaming
: undefined,
onModelSelected: prefixContext.onModelSelected,
onModelSelected,
},
});