Skip to content

Commit 9459944

Browse files
committed
fix(telegram): emit sent hooks for preview finals
1 parent dc06e4f commit 9459944

File tree

5 files changed

+128
-11
lines changed

5 files changed

+128
-11
lines changed

extensions/telegram/src/bot-message-dispatch.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
const createTelegramDraftStream = vi.hoisted(() => vi.fn());
1212
const dispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() => vi.fn());
1313
const deliverReplies = vi.hoisted(() => vi.fn());
14+
const emitDeliveredReplyHooks = vi.hoisted(() => vi.fn());
1415
const createForumTopicTelegram = vi.hoisted(() => vi.fn());
1516
const deleteMessageTelegram = vi.hoisted(() => vi.fn());
1617
const editForumTopicTelegram = vi.hoisted(() => vi.fn());
@@ -46,6 +47,7 @@ vi.mock("./draft-stream.js", () => ({
4647

4748
vi.mock("./bot/delivery.js", () => ({
4849
deliverReplies,
50+
emitDeliveredReplyHooks,
4951
}));
5052

5153
vi.mock("./send.js", () => ({
@@ -103,6 +105,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
103105
createTelegramDraftStream.mockClear();
104106
dispatchReplyWithBufferedBlockDispatcher.mockClear();
105107
deliverReplies.mockClear();
108+
emitDeliveredReplyHooks.mockClear();
106109
createForumTopicTelegram.mockClear();
107110
deleteMessageTelegram.mockClear();
108111
editForumTopicTelegram.mockClear();
@@ -2262,6 +2265,63 @@ describe("dispatchTelegramMessage draft streaming", () => {
22622265
expect(finalTextSentViaDeliverReplies).toBe(false);
22632266
});
22642267

2268+
it.each([
2269+
{
2270+
label: "preview-finalized",
2271+
configureEdit: () => {
2272+
editMessageTelegram.mockResolvedValue(undefined);
2273+
},
2274+
},
2275+
{
2276+
label: "preview-retained",
2277+
configureEdit: () => {
2278+
editMessageTelegram.mockRejectedValue(
2279+
new Error("timeout: request timed out after 30000ms"),
2280+
);
2281+
},
2282+
},
2283+
])(
2284+
"emits message:sent hooks when the final answer stays visible via %s",
2285+
async ({ configureEdit }) => {
2286+
const draftStream = createDraftStream(999);
2287+
createTelegramDraftStream.mockReturnValue(draftStream);
2288+
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
2289+
async ({ dispatcherOptions, replyOptions }) => {
2290+
await replyOptions?.onPartialReply?.({ text: "Streaming..." });
2291+
await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
2292+
return { queuedFinal: true };
2293+
},
2294+
);
2295+
deliverReplies.mockResolvedValue({ delivered: true });
2296+
configureEdit();
2297+
2298+
await dispatchWithContext({
2299+
context: createContext({
2300+
ctxPayload: { SessionKey: "agent:main:main" } as never,
2301+
}),
2302+
});
2303+
2304+
expect(emitDeliveredReplyHooks).toHaveBeenCalledWith(
2305+
expect.objectContaining({
2306+
sessionKeyForInternalHooks: "agent:main:main",
2307+
chatId: "123",
2308+
accountId: "default",
2309+
content: "Final answer",
2310+
success: true,
2311+
messageId: 999,
2312+
isGroup: false,
2313+
groupId: undefined,
2314+
}),
2315+
);
2316+
const finalTextSentViaDeliverReplies = deliverReplies.mock.calls.some((call: unknown[]) =>
2317+
(call[0] as { replies?: Array<{ text?: string }> })?.replies?.some(
2318+
(r: { text?: string }) => r.text === "Final answer",
2319+
),
2320+
);
2321+
expect(finalTextSentViaDeliverReplies).toBe(false);
2322+
},
2323+
);
2324+
22652325
it("falls back to sendPayload on pre-connect error during final edit", async () => {
22662326
const draftStream = createDraftStream(999);
22672327
createTelegramDraftStream.mockReturnValue(draftStream);

extensions/telegram/src/bot-message-dispatch.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
3030
import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js";
3131
import type { TelegramMessageContext } from "./bot-message-context.js";
3232
import type { TelegramBotOptions } from "./bot.js";
33-
import { deliverReplies } from "./bot/delivery.js";
33+
import { deliverReplies, emitDeliveredReplyHooks } from "./bot/delivery.js";
3434
import type { TelegramStreamMode } from "./bot/types.js";
3535
import type { TelegramInlineButtons } from "./button-types.js";
3636
import { createTelegramDraftStream } from "./draft-stream.js";
@@ -508,6 +508,18 @@ export const dispatchTelegramMessage = async ({
508508
markDelivered: () => {
509509
deliveryState.markDelivered();
510510
},
511+
onFinalPreviewDelivered: async ({ text, messageId }) => {
512+
emitDeliveredReplyHooks({
513+
sessionKeyForInternalHooks: ctxPayload.SessionKey,
514+
chatId: String(chatId),
515+
accountId: route.accountId,
516+
content: text,
517+
success: true,
518+
messageId,
519+
isGroup,
520+
groupId: isGroup ? String(chatId) : undefined,
521+
});
522+
},
511523
});
512524

513525
let queuedFinal = false;

extensions/telegram/src/bot/delivery.replies.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,25 @@ function emitMessageSentHooks(params: {
546546
);
547547
}
548548

549+
export function emitDeliveredReplyHooks(params: {
550+
sessionKeyForInternalHooks?: string;
551+
chatId: string;
552+
accountId?: string;
553+
content: string;
554+
success: boolean;
555+
error?: string;
556+
messageId?: number;
557+
isGroup?: boolean;
558+
groupId?: string;
559+
}): void {
560+
const hookRunner = getGlobalHookRunner();
561+
emitMessageSentHooks({
562+
hookRunner,
563+
enabled: hookRunner?.hasHooks("message_sent") ?? false,
564+
...params,
565+
});
566+
}
567+
549568
export async function deliverReplies(params: {
550569
replies: ReplyPayload[];
551570
chatId: string;
@@ -581,7 +600,6 @@ export async function deliverReplies(params: {
581600
const mediaLoader = params.mediaLoader ?? loadWebMedia;
582601
const hookRunner = getGlobalHookRunner();
583602
const hasMessageSendingHooks = hookRunner?.hasHooks("message_sending") ?? false;
584-
const hasMessageSentHooks = hookRunner?.hasHooks("message_sent") ?? false;
585603
const chunkText = buildChunkTextResolver({
586604
textLimit: params.textLimit,
587605
chunkMode: params.chunkMode ?? "length",
@@ -686,9 +704,7 @@ export async function deliverReplies(params: {
686704
firstDeliveredMessageId,
687705
});
688706

689-
emitMessageSentHooks({
690-
hookRunner,
691-
enabled: hasMessageSentHooks,
707+
emitDeliveredReplyHooks({
692708
sessionKeyForInternalHooks: params.sessionKeyForInternalHooks,
693709
chatId: params.chatId,
694710
accountId: params.accountId,
@@ -699,9 +715,7 @@ export async function deliverReplies(params: {
699715
groupId: params.mirrorGroupId,
700716
});
701717
} catch (error) {
702-
emitMessageSentHooks({
703-
hookRunner,
704-
enabled: hasMessageSentHooks,
718+
emitDeliveredReplyHooks({
705719
sessionKeyForInternalHooks: params.sessionKeyForInternalHooks,
706720
chatId: params.chatId,
707721
accountId: params.accountId,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { deliverReplies } from "./delivery.replies.js";
1+
export { deliverReplies, emitDeliveredReplyHooks } from "./delivery.replies.js";
22
export { resolveMedia } from "./delivery.resolve-media.js";

extensions/telegram/src/lane-delivery-text-deliverer.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ type CreateLaneTextDelivererParams = {
8383
deletePreviewMessage: (messageId: number) => Promise<void>;
8484
log: (message: string) => void;
8585
markDelivered: () => void;
86+
onFinalPreviewDelivered?: (params: {
87+
laneName: LaneName;
88+
text: string;
89+
messageId?: number;
90+
}) => Promise<void> | void;
8691
};
8792

8893
type DeliverLaneTextParams = {
@@ -167,6 +172,17 @@ function resolvePreviewTarget(params: ResolvePreviewTargetParams): PreviewTarget
167172
}
168173

169174
export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
175+
const notifyFinalPreviewDelivered = async (
176+
laneName: LaneName,
177+
text: string,
178+
messageId?: number,
179+
) => {
180+
await params.onFinalPreviewDelivered?.({
181+
laneName,
182+
text,
183+
messageId,
184+
});
185+
};
170186
const getLanePreviewText = (lane: DraftLaneState) => lane.lastPartialText;
171187
const markActivePreviewComplete = (laneName: LaneName) => {
172188
params.activePreviewLifecycleByLane[laneName] = "complete";
@@ -206,6 +222,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
206222
}
207223
args.lane.lastPartialText = args.text;
208224
params.markDelivered();
225+
await notifyFinalPreviewDelivered(args.laneName, args.text, materializedMessageId);
209226
return true;
210227
};
211228

@@ -232,13 +249,19 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
232249
args.lane.lastPartialText = args.text;
233250
}
234251
params.markDelivered();
252+
if (args.context === "final") {
253+
await notifyFinalPreviewDelivered(args.laneName, args.text, args.messageId);
254+
}
235255
return "edited";
236256
} catch (err) {
237257
if (isMessageNotModifiedError(err)) {
238258
params.log(
239259
`telegram: ${args.laneName} preview ${args.context} edit returned "message is not modified"; treating as delivered`,
240260
);
241261
params.markDelivered();
262+
if (args.context === "final") {
263+
await notifyFinalPreviewDelivered(args.laneName, args.text, args.messageId);
264+
}
242265
return "edited";
243266
}
244267
if (args.context === "final") {
@@ -247,6 +270,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
247270
`telegram: ${args.laneName} preview final edit failed after stop flush; keeping existing preview (${String(err)})`,
248271
);
249272
params.markDelivered();
273+
await notifyFinalPreviewDelivered(args.laneName, args.text, args.messageId);
250274
return "retained";
251275
}
252276
if (isSafeToRetrySendError(err)) {
@@ -261,6 +285,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
261285
`telegram: ${args.laneName} preview final edit target missing; keeping alternate preview without fallback (${String(err)})`,
262286
);
263287
params.markDelivered();
288+
await notifyFinalPreviewDelivered(args.laneName, args.text, args.messageId);
264289
return "retained";
265290
}
266291
params.log(
@@ -273,6 +298,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
273298
`telegram: ${args.laneName} preview final edit may have landed despite network error; keeping existing preview (${String(err)})`,
274299
);
275300
params.markDelivered();
301+
await notifyFinalPreviewDelivered(args.laneName, args.text, args.messageId);
276302
return "retained";
277303
}
278304
if (isTelegramClientRejection(err)) {
@@ -286,6 +312,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
286312
`telegram: ${args.laneName} preview final edit failed with ambiguous error; keeping existing preview to avoid duplicate (${String(err)})`,
287313
);
288314
params.markDelivered();
315+
await notifyFinalPreviewDelivered(args.laneName, args.text, args.messageId);
289316
return "retained";
290317
}
291318
params.log(
@@ -323,12 +350,12 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
323350
finalTextAlreadyLanded,
324351
retainAlternatePreviewOnMissingTarget,
325352
});
326-
const finalizePreview = (
353+
const finalizePreview = async (
327354
previewMessageId: number,
328355
finalTextAlreadyLanded: boolean,
329356
hadPreviewMessage: boolean,
330357
retainAlternatePreviewOnMissingTarget = false,
331-
): PreviewEditResult | Promise<PreviewEditResult> => {
358+
): Promise<PreviewEditResult> => {
332359
const currentPreviewText = previewTextSnapshot ?? getLanePreviewText(lane);
333360
const shouldSkipRegressive = shouldSkipRegressivePreviewUpdate({
334361
currentPreviewText,
@@ -338,6 +365,9 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
338365
});
339366
if (shouldSkipRegressive) {
340367
params.markDelivered();
368+
if (context === "final") {
369+
await notifyFinalPreviewDelivered(laneName, text, previewMessageId);
370+
}
341371
return "edited";
342372
}
343373
return editPreview(
@@ -389,6 +419,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
389419
`telegram: ${laneName} preview send may have landed despite missing message id; keeping to avoid duplicate`,
390420
);
391421
params.markDelivered();
422+
await notifyFinalPreviewDelivered(laneName, text);
392423
return "retained";
393424
}
394425
return "fallback";

0 commit comments

Comments
 (0)