fix: prevent Telegram preview stream cross-edit race (#23202)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 529abf209d
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
Ayaan Zaidi
2026-02-22 10:04:33 +05:30
committed by GitHub
parent 413f81b856
commit 63b4c500d9
5 changed files with 346 additions and 60 deletions

View File

@@ -155,7 +155,10 @@ export const dispatchTelegramMessage = async ({
lastPartialText: string;
hasStreamedMessage: boolean;
};
const createDraftLane = (enabled: boolean): DraftLaneState => {
type ArchivedPreview = { messageId: number; textSnapshot: string };
const archivedAnswerPreviews: ArchivedPreview[] = [];
const archivedReasoningPreviewIds: number[] = [];
const createDraftLane = (laneName: LaneName, enabled: boolean): DraftLaneState => {
const stream = enabled
? createTelegramDraftStream({
api: bot.api,
@@ -165,6 +168,21 @@ export const dispatchTelegramMessage = async ({
replyToMessageId: draftReplyToMessageId,
minInitialChars: draftMinInitialChars,
renderText: renderDraftPreview,
onSupersededPreview:
laneName === "answer" || laneName === "reasoning"
? (preview) => {
if (laneName === "reasoning") {
if (!archivedReasoningPreviewIds.includes(preview.messageId)) {
archivedReasoningPreviewIds.push(preview.messageId);
}
return;
}
archivedAnswerPreviews.push({
messageId: preview.messageId,
textSnapshot: preview.textSnapshot,
});
}
: undefined,
log: logVerbose,
warn: logVerbose,
})
@@ -176,15 +194,13 @@ export const dispatchTelegramMessage = async ({
};
};
const lanes: Record<LaneName, DraftLaneState> = {
answer: createDraftLane(canStreamAnswerDraft),
reasoning: createDraftLane(canStreamReasoningDraft),
answer: createDraftLane("answer", canStreamAnswerDraft),
reasoning: createDraftLane("reasoning", canStreamReasoningDraft),
};
const answerLane = lanes.answer;
const reasoningLane = lanes.reasoning;
let splitReasoningOnNextStream = false;
const reasoningStepState = createTelegramReasoningStepState();
type ArchivedPreview = { messageId: number; textSnapshot: string };
const archivedAnswerPreviews: ArchivedPreview[] = [];
type SplitLaneSegment = { lane: LaneName; text: string };
const splitTextIntoLaneSegments = (text?: string): SplitLaneSegment[] => {
const split = splitTelegramReasoningText(text);
@@ -434,6 +450,43 @@ export const dispatchTelegramMessage = async ({
return result.delivered;
};
type LaneDeliveryResult = "preview-finalized" | "preview-updated" | "sent" | "skipped";
const consumeArchivedAnswerPreviewForFinal = async (params: {
lane: DraftLaneState;
text: string;
payload: ReplyPayload;
previewButtons?: TelegramInlineButtons;
canEditViaPreview: boolean;
}): Promise<LaneDeliveryResult | undefined> => {
const archivedPreview = archivedAnswerPreviews.shift();
if (!archivedPreview) {
return undefined;
}
if (params.canEditViaPreview) {
const finalized = await tryUpdatePreviewForLane({
lane: params.lane,
laneName: "answer",
text: params.text,
previewButtons: params.previewButtons,
stopBeforeEdit: false,
skipRegressive: "existingOnly",
context: "final",
previewMessageId: archivedPreview.messageId,
previewTextSnapshot: archivedPreview.textSnapshot,
});
if (finalized) {
return "preview-finalized";
}
}
try {
await bot.api.deleteMessage(chatId, archivedPreview.messageId);
} catch (err) {
logVerbose(
`telegram: archived answer preview cleanup failed (${archivedPreview.messageId}): ${String(err)}`,
);
}
const delivered = await sendPayload(applyTextToPayload(params.payload, params.text));
return delivered ? "sent" : "skipped";
};
const deliverLaneText = async (params: {
laneName: LaneName;
text: string;
@@ -456,38 +509,32 @@ export const dispatchTelegramMessage = async ({
!hasMedia && text.length > 0 && text.length <= draftMaxChars && !payload.isError;
if (infoKind === "final") {
if (laneName === "answer" && archivedAnswerPreviews.length > 0) {
const archivedPreview = archivedAnswerPreviews.shift();
if (archivedPreview) {
if (canEditViaPreview) {
const finalized = await tryUpdatePreviewForLane({
lane,
laneName,
text,
previewButtons,
stopBeforeEdit: false,
skipRegressive: "existingOnly",
context: "final",
previewMessageId: archivedPreview.messageId,
previewTextSnapshot: archivedPreview.textSnapshot,
});
if (finalized) {
return "preview-finalized";
}
}
try {
await bot.api.deleteMessage(chatId, archivedPreview.messageId);
} catch (err) {
logVerbose(
`telegram: archived answer preview cleanup failed (${archivedPreview.messageId}): ${String(err)}`,
);
}
const delivered = await sendPayload(applyTextToPayload(payload, text));
return delivered ? "sent" : "skipped";
if (laneName === "answer") {
const archivedResult = await consumeArchivedAnswerPreviewForFinal({
lane,
text,
payload,
previewButtons,
canEditViaPreview,
});
if (archivedResult) {
return archivedResult;
}
}
if (canEditViaPreview && !finalizedPreviewByLane[laneName]) {
await flushDraftLane(lane);
if (laneName === "answer") {
const archivedResultAfterFlush = await consumeArchivedAnswerPreviewForFinal({
lane,
text,
payload,
previewButtons,
canEditViaPreview,
});
if (archivedResultAfterFlush) {
return archivedResultAfterFlush;
}
}
const finalized = await tryUpdatePreviewForLane({
lane,
laneName,
@@ -735,6 +782,15 @@ export const dispatchTelegramMessage = async ({
);
}
}
for (const messageId of archivedReasoningPreviewIds) {
try {
await bot.api.deleteMessage(chatId, messageId);
} catch (err) {
logVerbose(
`telegram: archived reasoning preview cleanup failed (${messageId}): ${String(err)}`,
);
}
}
}
let sentFallback = false;
if (