Skip to content

Commit 89b7fee

Browse files
VACIncobviyus
andauthored
fix: preserve Telegram forum topic last-route delivery (#53052) (thanks @VACInc)
* fix(telegram): preserve forum topic thread in last-route delivery * style(telegram): format last-route update * test(telegram): cover General topic last-route thread * test(telegram): align topic route helper * fix(telegram): skip bound-topic last-route writes --------- Co-authored-by: VACInc <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
1 parent 1c82b06 commit 89b7fee

File tree

4 files changed

+84
-32
lines changed

4 files changed

+84
-32
lines changed

extensions/telegram/src/bot-message-context.acp-bindings.test.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
22
import { createConfiguredBindingConversationRuntimeModuleMock } from "../../../test/helpers/extensions/configured-binding-runtime.js";
33

44
const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() => vi.fn());
5+
const recordInboundSessionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
56
const resolveConfiguredBindingRouteMock = vi.hoisted(() => vi.fn());
67

78
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
8-
return await createConfiguredBindingConversationRuntimeModuleMock(
9-
{
10-
ensureConfiguredBindingRouteReadyMock,
11-
resolveConfiguredBindingRouteMock,
12-
},
13-
importOriginal,
14-
);
9+
return {
10+
...(await createConfiguredBindingConversationRuntimeModuleMock(
11+
{
12+
ensureConfiguredBindingRouteReadyMock,
13+
resolveConfiguredBindingRouteMock,
14+
},
15+
importOriginal,
16+
)),
17+
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
18+
};
1519
});
1620

1721
let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest;
@@ -136,6 +140,7 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
136140

137141
beforeEach(() => {
138142
ensureConfiguredBindingRouteReadyMock.mockReset();
143+
recordInboundSessionMock.mockClear();
139144
resolveConfiguredBindingRouteMock.mockReset();
140145
resolveConfiguredBindingRouteMock.mockReturnValue(createConfiguredTelegramRoute());
141146
ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true });
@@ -155,6 +160,9 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
155160
expect(ctx?.route.accountId).toBe("work");
156161
expect(ctx?.route.matchedBy).toBe("binding.channel");
157162
expect(ctx?.route.sessionKey).toBe("agent:codex:acp:binding:telegram:work:abc123");
163+
expect(recordInboundSessionMock.mock.calls[0]?.[0]).toMatchObject({
164+
updateLastRoute: undefined,
165+
});
158166
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
159167
});
160168

extensions/telegram/src/bot-message-context.dm-topic-threadid.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889
7474
expect(updateLastRoute?.threadId).toBeUndefined();
7575
});
7676

77-
it("does not set updateLastRoute for group messages", async () => {
77+
it("passes threadId to updateLastRoute for forum topic group messages", async () => {
7878
const ctx = await buildCtx({
7979
message: {
80-
chat: { id: -1001234567890, type: "supergroup", title: "Test Group" },
80+
chat: { id: -1001234567890, type: "supergroup", title: "Test Group", is_forum: true },
8181
text: "@bot hello",
8282
message_thread_id: 99,
8383
},
@@ -88,7 +88,32 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889
8888
expect(ctx).not.toBeNull();
8989
expect(recordInboundSessionMock).toHaveBeenCalled();
9090

91-
// Check that updateLastRoute is undefined for groups
92-
expect(getRecordedUpdateLastRoute(0)).toBeUndefined();
91+
const updateLastRoute = getRecordedUpdateLastRoute(0) as
92+
| { threadId?: string; to?: string }
93+
| undefined;
94+
expect(updateLastRoute).toBeDefined();
95+
expect(updateLastRoute?.to).toBe("telegram:-1001234567890");
96+
expect(updateLastRoute?.threadId).toBe("99");
97+
});
98+
99+
it("passes threadId to updateLastRoute for the forum General topic", async () => {
100+
const ctx = await buildCtx({
101+
message: {
102+
chat: { id: -1001234567890, type: "supergroup", title: "Test Group", is_forum: true },
103+
text: "@bot hello",
104+
},
105+
options: { forceWasMentioned: true },
106+
resolveGroupActivation: () => true,
107+
});
108+
109+
expect(ctx).not.toBeNull();
110+
expect(recordInboundSessionMock).toHaveBeenCalled();
111+
112+
const updateLastRoute = getRecordedUpdateLastRoute(0) as
113+
| { threadId?: string; to?: string }
114+
| undefined;
115+
expect(updateLastRoute).toBeDefined();
116+
expect(updateLastRoute?.to).toBe("telegram:-1001234567890");
117+
expect(updateLastRoute?.threadId).toBe("1");
93118
});
94119
});

extensions/telegram/src/bot-message-context.session.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -261,32 +261,44 @@ export async function buildTelegramInboundContextPayload(params: {
261261
route,
262262
sessionKey: route.sessionKey,
263263
});
264+
const shouldPersistGroupLastRouteThread = isGroup && route.matchedBy !== "binding.channel";
265+
const updateLastRouteThreadId = isGroup
266+
? shouldPersistGroupLastRouteThread && resolvedThreadId != null
267+
? String(resolvedThreadId)
268+
: undefined
269+
: dmThreadId != null
270+
? String(dmThreadId)
271+
: undefined;
264272

265273
await recordInboundSession({
266274
storePath,
267275
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
268276
ctx: ctxPayload,
269-
updateLastRoute: !isGroup
270-
? {
271-
sessionKey: updateLastRouteSessionKey,
272-
channel: "telegram",
273-
to: `telegram:${chatId}`,
274-
accountId: route.accountId,
275-
threadId: dmThreadId != null ? String(dmThreadId) : undefined,
276-
mainDmOwnerPin:
277-
updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && senderId
278-
? {
279-
ownerRecipient: pinnedMainDmOwner,
280-
senderRecipient: senderId,
281-
onSkip: ({ ownerRecipient, senderRecipient }) => {
282-
logVerbose(
283-
`telegram: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
284-
);
285-
},
286-
}
287-
: undefined,
288-
}
289-
: undefined,
277+
updateLastRoute:
278+
!isGroup || updateLastRouteThreadId != null
279+
? {
280+
sessionKey: updateLastRouteSessionKey,
281+
channel: "telegram",
282+
to: `telegram:${chatId}`,
283+
accountId: route.accountId,
284+
threadId: updateLastRouteThreadId,
285+
mainDmOwnerPin:
286+
!isGroup &&
287+
updateLastRouteSessionKey === route.mainSessionKey &&
288+
pinnedMainDmOwner &&
289+
senderId
290+
? {
291+
ownerRecipient: pinnedMainDmOwner,
292+
senderRecipient: senderId,
293+
onSkip: ({ ownerRecipient, senderRecipient }) => {
294+
logVerbose(
295+
`telegram: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
296+
);
297+
},
298+
}
299+
: undefined,
300+
}
301+
: undefined,
290302
onRecordError: (err) => {
291303
logVerbose(`telegram: failed updating session meta: ${String(err)}`);
292304
},

extensions/telegram/src/bot-message-context.thread-binding.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
22

33
const hoisted = vi.hoisted(() => {
44
const resolveByConversationMock = vi.fn();
5+
const recordInboundSessionMock = vi.fn().mockResolvedValue(undefined);
56
const touchMock = vi.fn();
67
return {
8+
recordInboundSessionMock,
79
resolveByConversationMock,
810
touchMock,
911
};
@@ -13,6 +15,7 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
1315
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
1416
return {
1517
...actual,
18+
recordInboundSession: (...args: unknown[]) => hoisted.recordInboundSessionMock(...args),
1619
getSessionBindingService: () => ({
1720
bind: vi.fn(),
1821
getCapabilities: vi.fn(),
@@ -34,6 +37,7 @@ describe("buildTelegramMessageContext bound conversation override", () => {
3437
});
3538

3639
beforeEach(() => {
40+
hoisted.recordInboundSessionMock.mockClear();
3741
hoisted.resolveByConversationMock.mockReset().mockReturnValue(null);
3842
hoisted.touchMock.mockReset();
3943
});
@@ -63,6 +67,9 @@ describe("buildTelegramMessageContext bound conversation override", () => {
6367
conversationId: "-100200300:topic:77",
6468
});
6569
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:codex-acp:session-1");
70+
expect(hoisted.recordInboundSessionMock.mock.calls[0]?.[0]).toMatchObject({
71+
updateLastRoute: undefined,
72+
});
6673
expect(hoisted.touchMock).toHaveBeenCalledWith("default:-100200300:topic:77", undefined);
6774
});
6875

0 commit comments

Comments
 (0)