fix: stabilize Telegram draft boundaries and suppress NO_REPLY lead leaks (#33169)

* fix: stabilize telegram draft stream message boundaries

* fix: suppress NO_REPLY lead-fragment leaks

* fix: keep underscore guard for non-NO_REPLY prefixes

* fix: skip assistant-start rotation only after real lane rotation

* fix: preserve finalized state when pre-rotation does not force

* fix: reset finalized preview state on message-start boundary

* fix: document Telegram draft boundary + NO_REPLY reliability updates (#33169) (thanks @obviyus)
This commit is contained in:
Ayaan Zaidi
2026-03-03 22:49:33 +05:30
committed by GitHub
parent a7a9a3d3c8
commit 3d998828b9
8 changed files with 212 additions and 137 deletions

View File

@@ -225,16 +225,20 @@ export const dispatchTelegramMessage = async ({
stream,
lastPartialText: "",
hasStreamedMessage: false,
previewRevisionBaseline: stream?.previewRevision?.() ?? 0,
};
};
const lanes: Record<LaneName, DraftLaneState> = {
answer: createDraftLane("answer", canStreamAnswerDraft),
reasoning: createDraftLane("reasoning", canStreamReasoningDraft),
};
const finalizedPreviewByLane: Record<LaneName, boolean> = {
answer: false,
reasoning: false,
};
const answerLane = lanes.answer;
const reasoningLane = lanes.reasoning;
let splitReasoningOnNextStream = false;
let skipNextAnswerMessageStartRotation = false;
const reasoningStepState = createTelegramReasoningStepState();
type SplitLaneSegment = { lane: LaneName; text: string };
type SplitLaneSegmentsResult = {
@@ -260,7 +264,29 @@ export const dispatchTelegramMessage = async ({
const resetDraftLaneState = (lane: DraftLaneState) => {
lane.lastPartialText = "";
lane.hasStreamedMessage = false;
lane.previewRevisionBaseline = lane.stream?.previewRevision?.() ?? lane.previewRevisionBaseline;
};
const rotateAnswerLaneForNewAssistantMessage = () => {
let didForceNewMessage = false;
if (answerLane.hasStreamedMessage) {
const previewMessageId = answerLane.stream?.messageId();
// Only archive previews that still need a matching final text update.
// Once a preview has already been finalized, archiving it here causes
// cleanup to delete a user-visible final message on later media-only turns.
if (typeof previewMessageId === "number" && !finalizedPreviewByLane.answer) {
archivedAnswerPreviews.push({
messageId: previewMessageId,
textSnapshot: answerLane.lastPartialText,
});
}
answerLane.stream?.forceNewMessage();
didForceNewMessage = true;
}
resetDraftLaneState(answerLane);
if (didForceNewMessage) {
// New assistant message boundary: this lane now tracks a fresh preview lifecycle.
finalizedPreviewByLane.answer = false;
}
return didForceNewMessage;
};
const updateDraftFromPartial = (lane: DraftLaneState, text: string | undefined) => {
const laneStream = lane.stream;
@@ -287,6 +313,13 @@ export const dispatchTelegramMessage = async ({
};
const ingestDraftLaneSegments = (text: string | undefined) => {
const split = splitTextIntoLaneSegments(text);
const hasAnswerSegment = split.segments.some((segment) => segment.lane === "answer");
if (hasAnswerSegment && finalizedPreviewByLane.answer) {
// Some providers can emit the first partial of a new assistant message before
// onAssistantMessageStart() arrives. Rotate preemptively so we do not edit
// the previously finalized preview message with the next message's text.
skipNextAnswerMessageStartRotation = rotateAnswerLaneForNewAssistantMessage();
}
for (const segment of split.segments) {
if (segment.lane === "reasoning") {
reasoningStepState.noteReasoningHint();
@@ -376,10 +409,6 @@ export const dispatchTelegramMessage = async ({
? ctxPayload.ReplyToBody.trim() || undefined
: undefined;
const deliveryState = createLaneDeliveryStateTracker();
const finalizedPreviewByLane: Record<LaneName, boolean> = {
answer: false,
reasoning: false,
};
const clearGroupHistory = () => {
if (isGroup && historyKey) {
clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
@@ -599,21 +628,16 @@ export const dispatchTelegramMessage = async ({
onAssistantMessageStart: answerLane.stream
? async () => {
reasoningStepState.resetForNextStep();
if (answerLane.hasStreamedMessage) {
const previewMessageId = answerLane.stream?.messageId();
// Only archive previews that still need a matching final text update.
// Once a preview has already been finalized, archiving it here causes
// cleanup to delete a user-visible final message on later media-only turns.
if (typeof previewMessageId === "number" && !finalizedPreviewByLane.answer) {
archivedAnswerPreviews.push({
messageId: previewMessageId,
textSnapshot: answerLane.lastPartialText,
});
}
answerLane.stream?.forceNewMessage();
if (skipNextAnswerMessageStartRotation) {
skipNextAnswerMessageStartRotation = false;
finalizedPreviewByLane.answer = false;
return;
}
resetDraftLaneState(answerLane);
// New assistant message boundary: this lane now tracks a fresh preview lifecycle.
rotateAnswerLaneForNewAssistantMessage();
// Message-start is an explicit assistant-message boundary.
// Even when no forceNewMessage happened (e.g. prior answer had no
// streamed partials), the next partial belongs to a fresh lifecycle
// and must not trigger late pre-rotation mid-message.
finalizedPreviewByLane.answer = false;
}
: undefined,