mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 16:14:58 +00:00
fix: clear Telegram DM draft after materialize (#36746) (thanks @joelnishanth)
This commit is contained in:
@@ -133,6 +133,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram/draft-stream boundary stability: materialize DM draft previews at assistant-message/tool boundaries, serialize lane-boundary callbacks before final delivery, and scope preview cleanup to the active preview so multi-step Telegram streams no longer lose, overwrite, or leave stale preview bubbles. (#33842) Thanks @ngutman.
|
- Telegram/draft-stream boundary stability: materialize DM draft previews at assistant-message/tool boundaries, serialize lane-boundary callbacks before final delivery, and scope preview cleanup to the active preview so multi-step Telegram streams no longer lose, overwrite, or leave stale preview bubbles. (#33842) Thanks @ngutman.
|
||||||
- Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils.
|
- Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils.
|
||||||
- Telegram/DM draft final delivery: materialize text-only `sendMessageDraft` previews into one permanent final message and skip duplicate final payload sends, while preserving fallback behavior when materialization fails. (#34318) Thanks @Brotherinlaw-13.
|
- Telegram/DM draft final delivery: materialize text-only `sendMessageDraft` previews into one permanent final message and skip duplicate final payload sends, while preserving fallback behavior when materialization fails. (#34318) Thanks @Brotherinlaw-13.
|
||||||
|
- Telegram/DM draft duplicate display: clear stale DM draft previews after materializing the real final message, including threadless fallback when DM topic lookup fails, so partial streaming no longer briefly shows duplicate replies. (#36746) Thanks @joelnishanth.
|
||||||
- Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.
|
- Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.
|
||||||
- Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.
|
- Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.
|
||||||
- Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.
|
- Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.
|
||||||
|
|||||||
@@ -239,6 +239,27 @@ describe("createTelegramDraftStream", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears draft after materializing to avoid duplicate display in DM", async () => {
|
||||||
|
const api = createMockDraftApi();
|
||||||
|
const stream = createDraftStream(api, {
|
||||||
|
thread: { id: 42, scope: "dm" },
|
||||||
|
previewTransport: "draft",
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.update("Hello");
|
||||||
|
await stream.flush();
|
||||||
|
const materializedId = await stream.materialize?.();
|
||||||
|
|
||||||
|
expect(materializedId).toBe(17);
|
||||||
|
expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 42 });
|
||||||
|
// Draft should be cleared with empty string after real message is sent.
|
||||||
|
const draftCalls = api.sendMessageDraft.mock.calls;
|
||||||
|
const clearCall = draftCalls.find((call) => call[2] === "");
|
||||||
|
expect(clearCall).toBeDefined();
|
||||||
|
expect(clearCall?.[0]).toBe(123);
|
||||||
|
expect(clearCall?.[3]).toEqual({ message_thread_id: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
it("retries materialize send without thread when dm thread lookup fails", async () => {
|
it("retries materialize send without thread when dm thread lookup fails", async () => {
|
||||||
const api = createMockDraftApi();
|
const api = createMockDraftApi();
|
||||||
api.sendMessage
|
api.sendMessage
|
||||||
@@ -258,6 +279,10 @@ describe("createTelegramDraftStream", () => {
|
|||||||
expect(materializedId).toBe(55);
|
expect(materializedId).toBe(55);
|
||||||
expect(api.sendMessage).toHaveBeenNthCalledWith(1, 123, "Hello", { message_thread_id: 42 });
|
expect(api.sendMessage).toHaveBeenNthCalledWith(1, 123, "Hello", { message_thread_id: 42 });
|
||||||
expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "Hello", undefined);
|
expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "Hello", undefined);
|
||||||
|
const draftCalls = api.sendMessageDraft.mock.calls;
|
||||||
|
const clearCall = draftCalls.find((call) => call[2] === "");
|
||||||
|
expect(clearCall).toBeDefined();
|
||||||
|
expect(clearCall?.[3]).toBeUndefined();
|
||||||
expect(warn).toHaveBeenCalledWith(
|
expect(warn).toHaveBeenCalledWith(
|
||||||
"telegram stream preview materialize send failed with message_thread_id, retrying without thread",
|
"telegram stream preview materialize send failed with message_thread_id, retrying without thread",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -150,13 +150,16 @@ export function createTelegramDraftStream(params: {
|
|||||||
parse_mode: sendArgs.renderedParseMode,
|
parse_mode: sendArgs.renderedParseMode,
|
||||||
}
|
}
|
||||||
: replyParams;
|
: replyParams;
|
||||||
|
const usedThreadParams =
|
||||||
|
"message_thread_id" in (sendParams ?? {}) &&
|
||||||
|
typeof (sendParams as { message_thread_id?: unknown }).message_thread_id === "number";
|
||||||
try {
|
try {
|
||||||
return await params.api.sendMessage(chatId, sendArgs.renderedText, sendParams);
|
return {
|
||||||
|
sent: await params.api.sendMessage(chatId, sendArgs.renderedText, sendParams),
|
||||||
|
usedThreadParams,
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const hasThreadParam =
|
if (!usedThreadParams || !THREAD_NOT_FOUND_RE.test(String(err))) {
|
||||||
"message_thread_id" in (sendParams ?? {}) &&
|
|
||||||
typeof (sendParams as { message_thread_id?: unknown }).message_thread_id === "number";
|
|
||||||
if (!hasThreadParam || !THREAD_NOT_FOUND_RE.test(String(err))) {
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const threadlessParams = {
|
const threadlessParams = {
|
||||||
@@ -164,11 +167,14 @@ export function createTelegramDraftStream(params: {
|
|||||||
};
|
};
|
||||||
delete threadlessParams.message_thread_id;
|
delete threadlessParams.message_thread_id;
|
||||||
params.warn?.(sendArgs.fallbackWarnMessage);
|
params.warn?.(sendArgs.fallbackWarnMessage);
|
||||||
return await params.api.sendMessage(
|
return {
|
||||||
chatId,
|
sent: await params.api.sendMessage(
|
||||||
sendArgs.renderedText,
|
chatId,
|
||||||
Object.keys(threadlessParams).length > 0 ? threadlessParams : undefined,
|
sendArgs.renderedText,
|
||||||
);
|
Object.keys(threadlessParams).length > 0 ? threadlessParams : undefined,
|
||||||
|
),
|
||||||
|
usedThreadParams: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const sendMessageTransportPreview = async ({
|
const sendMessageTransportPreview = async ({
|
||||||
@@ -186,7 +192,7 @@ export function createTelegramDraftStream(params: {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const sent = await sendRenderedMessageWithThreadFallback({
|
const { sent } = await sendRenderedMessageWithThreadFallback({
|
||||||
renderedText,
|
renderedText,
|
||||||
renderedParseMode,
|
renderedParseMode,
|
||||||
fallbackWarnMessage:
|
fallbackWarnMessage:
|
||||||
@@ -369,7 +375,7 @@ export function createTelegramDraftStream(params: {
|
|||||||
}
|
}
|
||||||
const renderedParseMode = lastSentText ? lastSentParseMode : undefined;
|
const renderedParseMode = lastSentText ? lastSentParseMode : undefined;
|
||||||
try {
|
try {
|
||||||
const sent = await sendRenderedMessageWithThreadFallback({
|
const { sent, usedThreadParams } = await sendRenderedMessageWithThreadFallback({
|
||||||
renderedText,
|
renderedText,
|
||||||
renderedParseMode,
|
renderedParseMode,
|
||||||
fallbackWarnMessage:
|
fallbackWarnMessage:
|
||||||
@@ -378,6 +384,20 @@ export function createTelegramDraftStream(params: {
|
|||||||
const sentId = sent?.message_id;
|
const sentId = sent?.message_id;
|
||||||
if (typeof sentId === "number" && Number.isFinite(sentId)) {
|
if (typeof sentId === "number" && Number.isFinite(sentId)) {
|
||||||
streamMessageId = Math.trunc(sentId);
|
streamMessageId = Math.trunc(sentId);
|
||||||
|
// Clear the draft so Telegram's input area doesn't briefly show a
|
||||||
|
// stale copy alongside the newly materialized real message.
|
||||||
|
if (resolvedDraftApi != null && streamDraftId != null) {
|
||||||
|
const clearDraftId = streamDraftId;
|
||||||
|
const clearThreadParams =
|
||||||
|
usedThreadParams && threadParams?.message_thread_id != null
|
||||||
|
? { message_thread_id: threadParams.message_thread_id }
|
||||||
|
: undefined;
|
||||||
|
try {
|
||||||
|
await resolvedDraftApi(chatId, clearDraftId, "", clearThreadParams);
|
||||||
|
} catch {
|
||||||
|
// Best-effort cleanup; draft clear failure is cosmetic.
|
||||||
|
}
|
||||||
|
}
|
||||||
return streamMessageId;
|
return streamMessageId;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user