fix(telegram): rotate previews after visible tool output

This commit is contained in:
Vincent Koc
2026-05-05 16:08:21 -07:00
parent dd643b52df
commit 1470b439e2
2 changed files with 12 additions and 7 deletions

View File

@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
- Voice Call/realtime: add opt-in OpenClaw agent voice context capsules and consult-cadence guidance so Gemini/OpenAI realtime calls can sound like the configured agent without consulting the full agent on every ordinary turn. Thanks @scoootscooob.
- Docker/Gateway: harden the gateway container by dropping `NET_RAW` and `NET_ADMIN` capabilities and enabling `no-new-privileges` in the bundled `docker-compose.yml`. Thanks @VintageAyu.
- Telegram: accept plugin-owned numeric forum-topic targets in the agent message tool and keep reply-dispatch provider chunks behind a real stable runtime alias during in-place package updates. Fixes #77137. Thanks @richardmqq.
- Telegram/streaming: keep draft preview rotation from reusing a pre-tool assistant preview after visible tool or media output lands between compaction replay and the next assistant message. Thanks @vincentkoc.
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
- TTS/telephony: honor provider voice/model overrides in telephony synthesis providers so Google Meet agent speech logs match the backend that actually produced the audio. Thanks @vincentkoc.
- Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc.

View File

@@ -555,6 +555,7 @@ export const dispatchTelegramMessage = async ({
let splitReasoningOnNextStream = false;
let skipNextAnswerMessageStartRotation = false;
let pendingCompactionReplayBoundary = false;
let discardAnswerPreviewOnNextRotation = false;
let draftLaneEventQueue = Promise.resolve();
const reasoningStepState = createTelegramReasoningStepState();
const enqueueDraftLaneEvent = (task: () => Promise<void>): Promise<void> => {
@@ -600,6 +601,7 @@ export const dispatchTelegramMessage = async ({
const materializedId = await answerLane.stream?.materialize?.();
const previewMessageId = materializedId ?? answerLane.stream?.messageId();
if (
!discardAnswerPreviewOnNextRotation &&
typeof previewMessageId === "number" &&
activePreviewLifecycleByLane.answer === "transient"
) {
@@ -613,6 +615,7 @@ export const dispatchTelegramMessage = async ({
answerLane.stream?.forceNewMessage();
didForceNewMessage = true;
}
discardAnswerPreviewOnNextRotation = false;
resetDraftLaneState(answerLane);
answerLaneHasAssistantContent = false;
if (didForceNewMessage) {
@@ -967,11 +970,12 @@ export const dispatchTelegramMessage = async ({
if (isDispatchSuperseded()) {
return;
}
const clearPendingCompactionReplayBoundaryOnVisibleBoundary = (
didDeliver: boolean,
) => {
const markVisibleNonPreviewBoundary = (didDeliver: boolean) => {
if (didDeliver && info.kind !== "final") {
pendingCompactionReplayBoundary = false;
if (answerLane.hasStreamedMessage) {
discardAnswerPreviewOnNextRotation = true;
}
}
};
if (payload.isError === true) {
@@ -1047,6 +1051,8 @@ export const dispatchTelegramMessage = async ({
});
if (info.kind === "final") {
emitPreviewFinalizedHook(result);
} else if (segment.lane === "answer" && result.kind === "sent") {
markVisibleNonPreviewBoundary(true);
}
if (segment.lane === "reasoning") {
if (result.kind !== "skipped") {
@@ -1069,7 +1075,7 @@ export const dispatchTelegramMessage = async ({
if (reply.hasMedia) {
const payloadWithoutSuppressedReasoning =
typeof payload.text === "string" ? { ...payload, text: "" } : payload;
clearPendingCompactionReplayBoundaryOnVisibleBoundary(
markVisibleNonPreviewBoundary(
await sendPayload(payloadWithoutSuppressedReasoning),
);
}
@@ -1093,9 +1099,7 @@ export const dispatchTelegramMessage = async ({
}
return;
}
clearPendingCompactionReplayBoundaryOnVisibleBoundary(
await sendPayload(payload),
);
markVisibleNonPreviewBoundary(await sendPayload(payload));
if (info.kind === "final") {
await flushBufferedFinalAnswer();
pendingCompactionReplayBoundary = false;