Skip to content

Commit bacc18a

Browse files
clawsweeper[bot]jrwrestTakhoffman
authored
Log Telegram outbound delivery success (#83247)
Summary: - The PR adds info-level Telegram outbound send success logs for text/media sends, tracks accepted threadless ... s, and loads the OpenAI Codex external auth overlay for Codex plugin-harness runs with regression coverage. - Reproducibility: yes. there is a source-level reproduction path: the branch adds focused tests for Telegram ... mission/privacy and Codex auth overlay selection. I did not execute those tests in this read-only checkout. Automerge notes: - PR branch already contained follow-up commit before automerge: Use Codex auth overlay for scoped Codex runs - PR branch already contained follow-up commit before automerge: Add regression tests for Codex auth and Telegram send logs - PR branch already contained follow-up commit before automerge: Log Telegram outbound delivery success Validation: - ClawSweeper review passed for head b860487. - Required merge gates passed before the squash merge. Prepared head SHA: b860487 Review: #83247 (comment) Co-authored-by: jrwrest <[email protected]> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <[email protected]>
1 parent 9a5f2f6 commit bacc18a

5 files changed

Lines changed: 468 additions & 13 deletions

File tree

extensions/telegram/src/send.test.ts

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const {
2626
maybePersistResolvedTelegramTarget,
2727
probeVideoDimensions,
2828
} = getTelegramSendTestMocks();
29+
const telegramSendModule = await importTelegramSendModule();
30+
const { resetLogger, setLoggerOverride } = await import("openclaw/plugin-sdk/runtime-env");
2931
const {
3032
buildInlineKeyboard,
3133
createForumTopicTelegram,
@@ -40,7 +42,7 @@ const {
4042
sendPollTelegram,
4143
sendStickerTelegram,
4244
unpinMessageTelegram,
43-
} = await importTelegramSendModule();
45+
} = telegramSendModule;
4446

4547
const TELEGRAM_TEST_CFG = {};
4648

@@ -163,6 +165,26 @@ function expectPersistedTarget(fields: Record<string, unknown>): void {
163165
}
164166
}
165167

168+
let logCaptureCounter = 0;
169+
170+
function captureInfoLogs(): string {
171+
logCaptureCounter += 1;
172+
const logFile = `/tmp/openclaw-telegram-send-log-${process.pid}-${logCaptureCounter}.jsonl`;
173+
fs.rmSync(logFile, { force: true });
174+
setLoggerOverride({ level: "info", consoleLevel: "silent", file: logFile });
175+
return logFile;
176+
}
177+
178+
function capturedLogText(logFile: string): string {
179+
return fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
180+
}
181+
182+
afterEach(() => {
183+
setLoggerOverride(null);
184+
resetLogger();
185+
vi.restoreAllMocks();
186+
});
187+
166188
describe("sent-message-cache", () => {
167189
afterEach(() => {
168190
vi.useRealTimers();
@@ -1996,7 +2018,151 @@ describe("sendMessageTelegram", () => {
19962018
});
19972019
});
19982020

2021+
it("logs successful outbound text delivery without the message body", async () => {
2022+
const logFile = captureInfoLogs();
2023+
const chatId = "-1001234567890";
2024+
const body = "incident reply body should stay private";
2025+
const sendMessage = vi.fn().mockResolvedValue({
2026+
message_id: 321,
2027+
chat: { id: chatId },
2028+
});
2029+
const api = { sendMessage } as unknown as {
2030+
sendMessage: typeof sendMessage;
2031+
};
2032+
2033+
await sendMessageTelegram(`telegram:group:${chatId}:topic:271`, body, {
2034+
cfg: TELEGRAM_TEST_CFG,
2035+
token: "tok",
2036+
accountId: "ops",
2037+
api,
2038+
replyToMessageId: 123,
2039+
silent: true,
2040+
});
2041+
2042+
const logs = capturedLogText(logFile);
2043+
expect(logs).toContain("outbound send ok");
2044+
expect(logs).toContain("accountId=ops");
2045+
expect(logs).toContain(`chatId=${chatId}`);
2046+
expect(logs).toContain("messageId=321");
2047+
expect(logs).toContain("operation=sendMessage");
2048+
expect(logs).toContain("threadId=271");
2049+
expect(logs).toContain("replyToMessageId=123");
2050+
expect(logs).toContain("silent=true");
2051+
expect(logs).toContain("chunkCount=1");
2052+
expect(logs).not.toContain(body);
2053+
});
2054+
2055+
it("logs threadless outbound text delivery after missing-thread fallback", async () => {
2056+
const logFile = captureInfoLogs();
2057+
const chatId = "-1001234567890";
2058+
const body = "fallback reply body should stay private";
2059+
const threadErr = new Error("400: Bad Request: message thread not found");
2060+
const sendMessage = vi
2061+
.fn()
2062+
.mockRejectedValueOnce(threadErr)
2063+
.mockResolvedValueOnce({
2064+
message_id: 322,
2065+
chat: { id: chatId },
2066+
});
2067+
const api = { sendMessage } as unknown as {
2068+
sendMessage: typeof sendMessage;
2069+
};
2070+
2071+
await sendMessageTelegram(`telegram:group:${chatId}:topic:271`, body, {
2072+
cfg: TELEGRAM_TEST_CFG,
2073+
token: "tok",
2074+
accountId: "ops",
2075+
api,
2076+
});
2077+
2078+
expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, body, {
2079+
parse_mode: "HTML",
2080+
message_thread_id: 271,
2081+
});
2082+
expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, body, {
2083+
parse_mode: "HTML",
2084+
});
2085+
const logs = capturedLogText(logFile);
2086+
expect(logs).toContain("outbound send ok");
2087+
expect(logs).toContain("messageId=322");
2088+
expect(logs).not.toContain("threadId=271");
2089+
expect(logs).not.toContain(body);
2090+
});
2091+
2092+
it("logs successful outbound media delivery without caption or media location", async () => {
2093+
const logFile = captureInfoLogs();
2094+
const chatId = "123";
2095+
const caption = "private media caption";
2096+
const mediaUrl = "https://example.com/private-photo.jpg";
2097+
const fileName = "private-photo.jpg";
2098+
const sendPhoto = vi.fn().mockResolvedValue({
2099+
message_id: 654,
2100+
chat: { id: chatId },
2101+
});
2102+
const api = { sendPhoto } as unknown as {
2103+
sendPhoto: typeof sendPhoto;
2104+
};
2105+
2106+
mockLoadedMedia({
2107+
buffer: Buffer.from("fake-image"),
2108+
contentType: "image/jpeg",
2109+
fileName,
2110+
});
2111+
2112+
await sendMessageTelegram(chatId, caption, {
2113+
cfg: TELEGRAM_TEST_CFG,
2114+
token: "tok",
2115+
accountId: "ops",
2116+
api,
2117+
mediaUrl,
2118+
messageThreadId: 45,
2119+
});
2120+
2121+
const logs = capturedLogText(logFile);
2122+
expect(logs).toContain("outbound send ok");
2123+
expect(logs).toContain("accountId=ops");
2124+
expect(logs).toContain(`chatId=${chatId}`);
2125+
expect(logs).toContain("messageId=654");
2126+
expect(logs).toContain("operation=sendPhoto");
2127+
expect(logs).toContain("deliveryKind=photo");
2128+
expect(logs).toContain("threadId=45");
2129+
expect(logs).not.toContain(caption);
2130+
expect(logs).not.toContain(mediaUrl);
2131+
expect(logs).not.toContain(fileName);
2132+
});
2133+
2134+
it("does not log outbound success when a chunked text delivery fails mid-send", async () => {
2135+
const logFile = captureInfoLogs();
2136+
const chatId = "123";
2137+
const body = "private chunked body ".repeat(260);
2138+
const sendMessage = vi
2139+
.fn()
2140+
.mockResolvedValueOnce({
2141+
message_id: 700,
2142+
chat: { id: chatId },
2143+
})
2144+
.mockRejectedValueOnce(new Error("telegram send failed"));
2145+
const api = { sendMessage } as unknown as {
2146+
sendMessage: typeof sendMessage;
2147+
};
2148+
2149+
await expect(
2150+
sendMessageTelegram(chatId, body, {
2151+
cfg: TELEGRAM_TEST_CFG,
2152+
token: "tok",
2153+
accountId: "ops",
2154+
api,
2155+
}),
2156+
).rejects.toThrow(/telegram send failed/);
2157+
2158+
expect(sendMessage).toHaveBeenCalledTimes(2);
2159+
const logs = capturedLogText(logFile);
2160+
expect(logs).not.toContain("outbound send ok");
2161+
expect(logs).not.toContain(body);
2162+
});
2163+
19992164
it("retries media sends without message_thread_id when thread is missing", async () => {
2165+
const logFile = captureInfoLogs();
20002166
const chatId = "-100123";
20012167
const threadErr = new Error("400: Bad Request: message thread not found");
20022168
const sendPhoto = vi
@@ -2044,6 +2210,10 @@ describe("sendMessageTelegram", () => {
20442210
},
20452211
);
20462212
expect(res.messageId).toBe("59");
2213+
const logs = capturedLogText(logFile);
2214+
expect(logs).toContain("outbound send ok");
2215+
expect(logs).toContain("messageId=59");
2216+
expect(logs).not.toContain("threadId=271");
20472217
});
20482218

20492219
it("defaults outbound media uploads to 100MB", async () => {

extensions/telegram/src/send.ts

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ type TelegramMessageLike = {
115115
chat?: { id?: string | number };
116116
};
117117

118+
type TelegramOutboundSuccessLogParams = {
119+
accountId: string;
120+
chatId: string;
121+
messageId: string;
122+
operation: string;
123+
deliveryKind?: string;
124+
messageThreadId?: number;
125+
replyToMessageId?: number;
126+
silent?: boolean;
127+
chunkCount?: number;
128+
};
129+
118130
type TelegramReactionOpts = {
119131
cfg: OpenClawConfig;
120132
token?: string;
@@ -182,6 +194,32 @@ function splitTelegramPlainTextFallback(text: string, chunkCount: number, limit:
182194
return chunks;
183195
}
184196

197+
function logTelegramOutboundSendOk(params: TelegramOutboundSuccessLogParams): void {
198+
const parts = [
199+
"telegram outbound send ok",
200+
`accountId=${params.accountId}`,
201+
`chatId=${params.chatId}`,
202+
`messageId=${params.messageId}`,
203+
`operation=${params.operation}`,
204+
];
205+
if (params.deliveryKind) {
206+
parts.push(`deliveryKind=${params.deliveryKind}`);
207+
}
208+
if (typeof params.messageThreadId === "number") {
209+
parts.push(`threadId=${params.messageThreadId}`);
210+
}
211+
if (typeof params.replyToMessageId === "number") {
212+
parts.push(`replyToMessageId=${params.replyToMessageId}`);
213+
}
214+
if (params.silent === true) {
215+
parts.push("silent=true");
216+
}
217+
if (typeof params.chunkCount === "number") {
218+
parts.push(`chunkCount=${params.chunkCount}`);
219+
}
220+
sendLogger.info(parts.join(" "));
221+
}
222+
185223
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
186224
const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i;
187225
const MESSAGE_NOT_MODIFIED_RE =
@@ -546,9 +584,9 @@ async function withTelegramThreadFallback<
546584
verbose: boolean | undefined,
547585
allowThreadlessRetry: boolean,
548586
attempt: (effectiveParams: TParams, effectiveLabel: string) => Promise<T>,
549-
): Promise<T> {
587+
): Promise<{ result: T; acceptedParams: TParams }> {
550588
try {
551-
return await attempt(params, label);
589+
return { result: await attempt(params, label), acceptedParams: params };
552590
} catch (err) {
553591
// Do not widen this fallback to cover "chat not found".
554592
// chat-not-found is routing/auth/membership/token; stripping thread IDs hides root cause.
@@ -565,7 +603,10 @@ async function withTelegramThreadFallback<
565603
);
566604
}
567605
const retriedParams = removeMessageThreadIdParam(params);
568-
return await attempt(retriedParams, `${label}-threadless`);
606+
return {
607+
result: await attempt(retriedParams, `${label}-threadless`),
608+
acceptedParams: retriedParams,
609+
};
569610
}
570611
}
571612

@@ -725,16 +766,36 @@ export async function sendMessageTelegram(
725766
): Promise<{ messageId: string; chatId: string }> => {
726767
let lastMessageId = "";
727768
let lastChatId = chatId;
769+
let lastAcceptedParams: TelegramThreadScopedParams | undefined;
770+
let sentChunkCount = 0;
728771
for (let index = 0; index < chunks.length; index += 1) {
729772
const chunk = chunks[index];
730773
if (!chunk) {
731774
continue;
732775
}
733-
const res = await sendTelegramTextChunk(chunk, buildTextParams(index === chunks.length - 1));
776+
const { result: res, acceptedParams } = await sendTelegramTextChunk(
777+
chunk,
778+
buildTextParams(index === chunks.length - 1),
779+
);
734780
const messageId = resolveTelegramMessageIdOrThrow(res, context);
735781
recordSentMessage(chatId, messageId, cfg);
736782
lastMessageId = String(messageId);
737783
lastChatId = String(res?.chat?.id ?? chatId);
784+
lastAcceptedParams = acceptedParams;
785+
sentChunkCount += 1;
786+
}
787+
if (lastMessageId) {
788+
logTelegramOutboundSendOk({
789+
accountId: account.accountId,
790+
chatId: lastChatId,
791+
messageId: lastMessageId,
792+
operation: "sendMessage",
793+
deliveryKind: "text",
794+
messageThreadId: lastAcceptedParams?.message_thread_id,
795+
replyToMessageId: opts.replyToMessageId,
796+
silent: opts.silent,
797+
chunkCount: sentChunkCount,
798+
});
738799
}
739800
return { messageId: lastMessageId, chatId: lastChatId };
740801
};
@@ -962,10 +1023,23 @@ export async function sendMessageTelegram(
9621023
};
9631024
})();
9641025

965-
const result = await sendMedia(mediaSender.label, mediaSender.sender);
1026+
const { result, acceptedParams } = await sendMedia(mediaSender.label, mediaSender.sender);
9661027
const mediaMessageId = resolveTelegramMessageIdOrThrow(result, "media send");
9671028
const resolvedChatId = String(result?.chat?.id ?? chatId);
9681029
recordSentMessage(chatId, mediaMessageId, cfg);
1030+
logTelegramOutboundSendOk({
1031+
accountId: account.accountId,
1032+
chatId: resolvedChatId,
1033+
messageId: String(mediaMessageId),
1034+
operation: `send${mediaSender.label
1035+
.split("_")
1036+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
1037+
.join("")}`,
1038+
deliveryKind: mediaSender.label,
1039+
messageThreadId: acceptedParams?.message_thread_id,
1040+
replyToMessageId: opts.replyToMessageId,
1041+
silent: opts.silent,
1042+
});
9691043
recordChannelActivity({
9701044
channel: "telegram",
9711045
accountId: account.accountId,
@@ -1532,7 +1606,7 @@ export async function sendStickerTelegram(
15321606

15331607
const stickerParams = hasThreadParams ? threadParams : undefined;
15341608

1535-
const result = await withTelegramThreadFallback(
1609+
const { result } = await withTelegramThreadFallback(
15361610
stickerParams,
15371611
"sticker",
15381612
opts.verbose,
@@ -1640,7 +1714,7 @@ export async function sendPollTelegram(
16401714
...(opts.silent === true ? { disable_notification: true } : {}),
16411715
};
16421716

1643-
const result = await withTelegramThreadFallback(
1717+
const { result } = await withTelegramThreadFallback(
16441718
pollParams,
16451719
"poll",
16461720
opts.verbose,

src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,9 @@ export const mockedEnsureAuthProfileStore = vi.fn(() => ({}));
224224
export const mockedEnsureAuthProfileStoreWithoutExternalProfiles = vi.fn(
225225
(_agentDir?: string, _options?: { allowKeychainPrompt?: boolean }) => ({}),
226226
);
227-
export const mockedResolveAuthProfileOrder = vi.fn(() => [] as string[]);
227+
export const mockedResolveAuthProfileOrder = vi.fn<(_params?: unknown) => string[]>(
228+
(_params?: unknown) => [],
229+
);
228230
export const mockedMarkAuthProfileSuccess = vi.fn(async () => {});
229231
export const mockedShouldPreferExplicitConfigApiKeyAuth = vi.fn(() => false);
230232

0 commit comments

Comments
 (0)