mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(telegram): dedupe visible assistant prompt context
This commit is contained in:
@@ -48,6 +48,7 @@ import {
|
||||
resolveAmbientTranscriptWatermarkKey,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
|
||||
import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js";
|
||||
import { resolveTelegramAccount, resolveTelegramMediaRuntimeOptions } from "./accounts.js";
|
||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||
@@ -187,13 +188,17 @@ type TelegramPromptContextMessageForDedupe = {
|
||||
function resolvePromptContextTextDedupeKey(
|
||||
message: TelegramPromptContextMessageForDedupe,
|
||||
): string | undefined {
|
||||
if (typeof message.body !== "string" || !message.body.trim()) {
|
||||
if (typeof message.body !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const visibleBody = stripInlineDirectiveTagsForDelivery(message.body).text.trim();
|
||||
if (!visibleBody) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof message.timestamp_ms !== "number" || !Number.isFinite(message.timestamp_ms)) {
|
||||
return undefined;
|
||||
}
|
||||
return `${message.timestamp_ms}:${message.body.trim()}`;
|
||||
return `${message.timestamp_ms}:${visibleBody}`;
|
||||
}
|
||||
|
||||
export const registerTelegramHandlers = ({
|
||||
|
||||
@@ -153,6 +153,7 @@ async function writeDirectTelegramTranscriptContext(params: {
|
||||
cfg: OpenClawConfig;
|
||||
storePath: string;
|
||||
chatId: number;
|
||||
role?: "assistant" | "user";
|
||||
senderId: number;
|
||||
sessionId: string;
|
||||
text: string;
|
||||
@@ -188,10 +189,10 @@ async function writeDirectTelegramTranscriptContext(params: {
|
||||
[
|
||||
JSON.stringify({ type: "session", id: params.sessionId }),
|
||||
JSON.stringify({
|
||||
id: "transcript-user-1",
|
||||
id: params.role === "assistant" ? "transcript-assistant-1" : "transcript-user-1",
|
||||
type: "message",
|
||||
message: {
|
||||
role: "user",
|
||||
role: params.role ?? "user",
|
||||
content: params.text,
|
||||
timestamp: params.timestamp,
|
||||
},
|
||||
@@ -2429,6 +2430,102 @@ describe("createTelegramBot", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("dedupes direct assistant transcript context against cached Telegram replies after stripping directives", async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
|
||||
const storePath = `/tmp/openclaw-telegram-dm-visible-dedupe-${process.pid}-${Date.now()}.json`;
|
||||
const config = {
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
} satisfies NonNullable<Parameters<typeof createTelegramBot>[0]["config"]>;
|
||||
|
||||
await rm(storePath, { force: true });
|
||||
await rm(`${storePath}.telegram-messages.json`, { force: true });
|
||||
try {
|
||||
loadConfig.mockReturnValue(config);
|
||||
createTelegramBot({ token: "tok", config });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const baseCtx = {
|
||||
me: { id: 999, username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
};
|
||||
const chatId = 7773;
|
||||
const senderId = 202;
|
||||
const visibleReply = "Yep - I'm here now.";
|
||||
const replyTimestampMs = 1_778_474_700_000;
|
||||
|
||||
await writeDirectTelegramTranscriptContext({
|
||||
cfg: config,
|
||||
storePath,
|
||||
chatId,
|
||||
role: "assistant",
|
||||
senderId,
|
||||
sessionId: "telegram-dm-assistant-visible-dedupe-session",
|
||||
text: `[[reply_to_current]]${visibleReply}`,
|
||||
timestamp: replyTimestampMs,
|
||||
});
|
||||
await handler({
|
||||
...baseCtx,
|
||||
message: {
|
||||
chat: { id: chatId, type: "private" },
|
||||
text: "still there?",
|
||||
date: 1_778_474_850,
|
||||
message_id: 741,
|
||||
from: { id: senderId, is_bot: false, first_name: "Kesava" },
|
||||
reply_to_message: {
|
||||
chat: { id: chatId, type: "private" },
|
||||
date: Math.floor(replyTimestampMs / 1000),
|
||||
from: { id: 999, is_bot: true, first_name: "OpenClaw" },
|
||||
message_id: 736,
|
||||
text: visibleReply,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = mockMsgContextArg(
|
||||
replySpy as unknown as MockCallSource,
|
||||
0,
|
||||
0,
|
||||
"replySpy call",
|
||||
);
|
||||
const [conversationContext] = requireArray(
|
||||
payload.UntrustedStructuredContext,
|
||||
"structured context",
|
||||
);
|
||||
const contextRecord = requireRecord(conversationContext, "conversation context");
|
||||
const contextPayload = requireRecord(contextRecord.payload, "conversation context payload");
|
||||
const messages = requireArray(contextPayload.messages, "conversation context messages").map(
|
||||
(message, index) => requireRecord(message, `conversation context message ${index + 1}`),
|
||||
);
|
||||
|
||||
expect(messages).toEqual([
|
||||
expect.objectContaining({
|
||||
body: visibleReply,
|
||||
is_reply_target: true,
|
||||
message_id: "736",
|
||||
sender: "OpenClaw",
|
||||
}),
|
||||
]);
|
||||
expect(messages.filter((message) => message.body === visibleReply)).toHaveLength(1);
|
||||
expect(JSON.stringify(messages)).not.toContain("[[reply_to_current]]");
|
||||
expect(messages.some((message) => String(message.message_id).startsWith("session:"))).toBe(
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
await rm(storePath, { force: true });
|
||||
await rm(`${storePath}.telegram-messages.json`, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("skips direct transcript context for hard reset messages", async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
|
||||
Reference in New Issue
Block a user