Skip to content

Commit cd2bba1

Browse files
committed
fix: finish telegram reply fallback landing (#52524) (thanks @moltbot886)
1 parent ea04de8 commit cd2bba1

File tree

9 files changed

+69
-25
lines changed

9 files changed

+69
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ Docs: https://docs.openclaw.ai
103103
- Gateway/restart: defer externally signaled unmanaged restarts through the in-process idle drain, and preserve the restored subagent run as remap fallback during orphan recovery so resumed sessions do not duplicate work. (#47719) Thanks @joeykrug.
104104
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
105105
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) Thanks @scoootscooob.
106+
- Mattermost/threading: honor `replyToMode: "off"` for already-threaded inbound posts so threaded follow-ups can fall back to top-level replies when configured. (#52543) Thanks @RichardCao.
107+
- Telegram/replies: set `allow_sending_without_reply` on reply-targeted sends and media-error notices so deleted parent messages no longer drop otherwise valid replies. (#52524) Thanks @moltbot886.
106108
- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc.
107109
- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc.
108110
- CLI/configure: clarify fresh-setup memory-search warnings so they say semantic recall needs at least one embedding provider, and scope the initial model allowlist picker to the provider selected in configure. Thanks @vincentkoc.

extensions/mattermost/src/mattermost/directory.test.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,11 @@ describe("mattermost directory", () => {
3838
it("deduplicates channels across enabled accounts and skips failing accounts", async () => {
3939
const clientA = {
4040
token: "token-a",
41-
request: vi
42-
.fn()
43-
.mockResolvedValueOnce([
44-
{ id: "chan-1", type: "O", name: "alerts", display_name: "Alerts" },
45-
{ id: "chan-2", type: "P", name: "ops", display_name: "Ops" },
46-
{ id: "chan-3", type: "D", name: "dm", display_name: "Direct" },
47-
]),
41+
request: vi.fn().mockResolvedValueOnce([
42+
{ id: "chan-1", type: "O", name: "alerts", display_name: "Alerts" },
43+
{ id: "chan-2", type: "P", name: "ops", display_name: "Ops" },
44+
{ id: "chan-3", type: "D", name: "dm", display_name: "Direct" },
45+
]),
4846
};
4947
const clientB = {
5048
token: "token-b",
@@ -86,11 +84,7 @@ describe("mattermost directory", () => {
8684
request: vi
8785
.fn()
8886
.mockResolvedValueOnce([{ id: "team-1" }])
89-
.mockResolvedValueOnce([
90-
{ user_id: "me-1" },
91-
{ user_id: "user-1" },
92-
{ user_id: "user-2" },
93-
])
87+
.mockResolvedValueOnce([{ user_id: "me-1" }, { user_id: "user-1" }, { user_id: "user-2" }])
9488
.mockResolvedValueOnce([
9589
{
9690
id: "user-1",

extensions/msteams/src/setup-surface.test.ts

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

33
const resolveMSTeamsUserAllowlist = vi.hoisted(() => vi.fn());
44
const resolveMSTeamsChannelAllowlist = vi.hoisted(() => vi.fn());
5-
const normalizeSecretInputString = vi.hoisted(() => vi.fn((value: unknown) => String(value ?? "").trim() || undefined));
5+
const normalizeSecretInputString = vi.hoisted(() =>
6+
vi.fn((value: unknown) => String(value ?? "").trim() || undefined),
7+
);
68
const hasConfiguredMSTeamsCredentials = vi.hoisted(() => vi.fn());
79
const resolveMSTeamsCredentials = vi.hoisted(() => vi.fn());
810

@@ -62,7 +64,7 @@ describe("msteams setup surface", () => {
6264

6365
hasConfiguredMSTeamsCredentials.mockReturnValue(false);
6466
expect(
65-
msteamsSetupWizard.status.resolveStatusLines({
67+
msteamsSetupWizard.status.resolveStatusLines?.({
6668
cfg: { channels: { msteams: {} } },
6769
} as never),
6870
).toEqual(["MS Teams: needs app credentials"]);

extensions/openshell/src/remote-fs-bridge.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ async function emulateRemoteShell(params: {
9393
return { stdout: await fs.readFile(params.args[0] ?? ""), stderr: Buffer.alloc(0), code: 0 };
9494
}
9595

96-
if (params.script === 'if [ -e "$1" ] || [ -L "$1" ]; then printf "1\\n"; else printf "0\\n"; fi') {
96+
if (
97+
params.script === 'if [ -e "$1" ] || [ -L "$1" ]; then printf "1\\n"; else printf "0\\n"; fi'
98+
) {
9799
const target = params.args[0] ?? "";
98100
const exists = await pathExistsOrSymlink(target);
99101
return { stdout: Buffer.from(exists ? "1\n" : "0\n"), stderr: Buffer.alloc(0), code: 0 };
@@ -129,7 +131,7 @@ async function emulateRemoteShell(params: {
129131
};
130132
}
131133

132-
if (params.script.includes('python3 /dev/fd/3 "$@" 3<<\'PY\'')) {
134+
if (params.script.includes("python3 /dev/fd/3 \"$@\" 3<<'PY'")) {
133135
await applyMutation(params.args, params.stdin);
134136
return { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), code: 0 };
135137
}

extensions/telegram/src/bot-handlers.runtime.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,8 +1016,10 @@ export const registerTelegramHandlers = ({
10161016
runtime,
10171017
fn: () =>
10181018
bot.api.sendMessage(chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, {
1019-
reply_to_message_id: msg.message_id,
1020-
allow_sending_without_reply: true,
1019+
reply_parameters: {
1020+
message_id: msg.message_id,
1021+
allow_sending_without_reply: true,
1022+
},
10211023
}),
10221024
}).catch(() => {});
10231025
}
@@ -1030,8 +1032,10 @@ export const registerTelegramHandlers = ({
10301032
runtime,
10311033
fn: () =>
10321034
bot.api.sendMessage(chatId, "⚠️ Failed to download media. Please try again.", {
1033-
reply_to_message_id: msg.message_id,
1034-
allow_sending_without_reply: true,
1035+
reply_parameters: {
1036+
message_id: msg.message_id,
1037+
allow_sending_without_reply: true,
1038+
},
10351039
}),
10361040
}).catch(() => {});
10371041
return;

extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,12 @@ describe("createTelegramBot channel_post media", () => {
231231
expect(sendMessageSpy).toHaveBeenCalledWith(
232232
1234,
233233
"⚠️ Failed to download media. Please try again.",
234-
{ reply_to_message_id: 411 },
234+
{
235+
reply_parameters: {
236+
message_id: 411,
237+
allow_sending_without_reply: true,
238+
},
239+
},
235240
);
236241
expect(replySpy).not.toHaveBeenCalled();
237242
} finally {

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ describe("deliverReplies", () => {
636636
expect.any(String),
637637
expect.objectContaining({
638638
reply_to_message_id: 500,
639+
allow_sending_without_reply: true,
639640
}),
640641
);
641642
expect(sendMessage).toHaveBeenCalledWith(
@@ -744,6 +745,7 @@ describe("deliverReplies", () => {
744745
expect(sendMessage.mock.calls[0][2]).toEqual(
745746
expect.objectContaining({
746747
reply_to_message_id: 77,
748+
allow_sending_without_reply: true,
747749
reply_markup: {
748750
inline_keyboard: [[{ text: "Ack", callback_data: "ack" }]],
749751
},
@@ -799,7 +801,10 @@ describe("deliverReplies", () => {
799801
expect(sendMessage.mock.calls.length).toBeGreaterThanOrEqual(2);
800802
// First chunk should have reply_to_message_id
801803
expect(sendMessage.mock.calls[0][2]).toEqual(
802-
expect.objectContaining({ reply_to_message_id: 700 }),
804+
expect.objectContaining({
805+
reply_to_message_id: 700,
806+
allow_sending_without_reply: true,
807+
}),
803808
);
804809
// Second chunk should NOT have reply_to_message_id
805810
expect(sendMessage.mock.calls[1][2]).not.toHaveProperty("reply_to_message_id");
@@ -826,7 +831,12 @@ describe("deliverReplies", () => {
826831
expect(sendMessage.mock.calls.length).toBeGreaterThanOrEqual(2);
827832
// Both chunks should have reply_to_message_id
828833
for (const call of sendMessage.mock.calls) {
829-
expect(call[2]).toEqual(expect.objectContaining({ reply_to_message_id: 800 }));
834+
expect(call[2]).toEqual(
835+
expect.objectContaining({
836+
reply_to_message_id: 800,
837+
allow_sending_without_reply: true,
838+
}),
839+
);
830840
}
831841
});
832842

@@ -854,7 +864,10 @@ describe("deliverReplies", () => {
854864
expect(sendPhoto).toHaveBeenCalledTimes(2);
855865
// First media should have reply_to_message_id
856866
expect(sendPhoto.mock.calls[0][2]).toEqual(
857-
expect.objectContaining({ reply_to_message_id: 900 }),
867+
expect.objectContaining({
868+
reply_to_message_id: 900,
869+
allow_sending_without_reply: true,
870+
}),
858871
);
859872
// Second media should NOT have reply_to_message_id
860873
expect(sendPhoto.mock.calls[1][2]).not.toHaveProperty("reply_to_message_id");

extensions/telegram/src/draft-stream.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,24 @@ describe("createTelegramDraftStream", () => {
218218
);
219219
});
220220

221+
it("keeps allow_sending_without_reply on message previews that target a reply", async () => {
222+
const api = createMockDraftApi();
223+
const stream = createDraftStream(api, {
224+
thread: { id: 42, scope: "dm" },
225+
previewTransport: "message",
226+
replyToMessageId: 411,
227+
});
228+
229+
stream.update("Hello");
230+
await stream.flush();
231+
232+
expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", {
233+
message_thread_id: 42,
234+
reply_to_message_id: 411,
235+
allow_sending_without_reply: true,
236+
});
237+
});
238+
221239
it("materializes draft previews using rendered HTML text", async () => {
222240
const api = createMockDraftApi();
223241
const stream = createDraftStream(api, {

extensions/telegram/src/draft-stream.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,11 @@ export function createTelegramDraftStream(params: {
125125
const threadParams = buildTelegramThreadParams(params.thread);
126126
const replyParams =
127127
params.replyToMessageId != null
128-
? { ...threadParams, reply_to_message_id: params.replyToMessageId, allow_sending_without_reply: true }
128+
? {
129+
...threadParams,
130+
reply_to_message_id: params.replyToMessageId,
131+
allow_sending_without_reply: true,
132+
}
129133
: threadParams;
130134
const resolvedDraftApi = prefersDraftTransport
131135
? resolveSendMessageDraftApi(params.api)

0 commit comments

Comments
 (0)