mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 06:31:11 +00:00
fix(telegram): recover incomplete preview finalization (#71554)
Fix Telegram partial-stream preview finalization so ambiguous final edit failures fall back to a final send when the visible preview is a strict prefix of the answer. Includes archived-preview regression coverage and generated config metadata refresh. Thanks @sahilsatralkar. Co-authored-by: Sahil Satralkar <62758655+sahilsatralkar@users.noreply.github.com>
This commit is contained in:
parent
e25b3c6056
commit
3064ea78ab
7 changed files with 62 additions and 6 deletions
|
|
@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
|
|||
- CLI/models: make `openclaw models scan` fall back to public OpenRouter free-model metadata when no `OPENROUTER_API_KEY` is configured, avoid config secret resolution for explicit `--no-probe` scans, and apply the scan timeout to the OpenRouter catalog request.
|
||||
- Feishu: keep streaming cards to one live card per turn, flush throttled card edits after meaningful text boundaries, and skip exact block/partial repeats so tool-heavy replies do not duplicate card output. Thanks @allan0509.
|
||||
- Feishu: finish the streaming-card duplicate closeout by stripping leaked reasoning tags, preserving cross-block partial snapshots, enabling topic-thread streaming cards, omitting the generic `main` card header, surfacing transient tool/compaction status, and cleaning streaming state after close failures. Thanks @sesame437, @Vicky-v7, @maoku-family, @Pengxiao-Wang, and @Maple778.
|
||||
- Telegram: recover incomplete partial-stream previews by falling back to a final send when an ambiguous final edit failure would otherwise retain a strict prefix of the answer. Fixes #71525. (#71554) Thanks @sahilsatralkar.
|
||||
- Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys.
|
||||
- Control UI/chat: collapse assistant token/model context details behind an explicit Context disclosure and show full dates in message footers, making historical transcript timing clear without noisy default metadata. (#71337) Thanks @BunsDev.
|
||||
- OpenAI/Codex OAuth: explain `unsupported_country_region_territory` token-exchange failures with a proxy/region hint instead of surfacing a generic OAuth error. Fixes #51175. (#71501) Thanks @vincentkoc and @wulala-xjj.
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
|||
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
|
||||
- `progress` maps to `partial` on Telegram (compat with cross-channel naming)
|
||||
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
|
||||
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
|
||||
- legacy `channels.telegram.streamMode` and boolean `streaming` values are detected; run `openclaw doctor --fix` to migrate them to `channels.telegram.streaming.mode`
|
||||
|
||||
Tool-progress preview updates are the short "Working..." lines shown while tools run, for example command execution, file reads, planning updates, or patch summaries. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later. To keep the edited preview for answer text but hide tool-progress lines, set:
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ Slack-only:
|
|||
|
||||
Legacy key migration:
|
||||
|
||||
- Telegram: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum.
|
||||
- Telegram: legacy `streamMode` and scalar/boolean `streaming` values are detected and migrated by doctor/config compatibility paths to `streaming.mode`.
|
||||
- Discord: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum.
|
||||
- Slack: `streamMode` auto-migrates to `streaming.mode`; boolean `streaming` auto-migrates to `streaming.mode` plus `streaming.nativeTransport`; legacy `nativeStreaming` auto-migrates to `streaming.nativeTransport`.
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const telegramChannelConfigUiHints = {
|
|||
},
|
||||
streaming: {
|
||||
label: "Telegram Streaming Mode",
|
||||
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are detected; run doctor --fix to migrate.',
|
||||
},
|
||||
"streaming.mode": {
|
||||
label: "Telegram Streaming Mode",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ function isMissingPreviewMessageError(err: unknown): boolean {
|
|||
return MESSAGE_NOT_FOUND_RE.test(extractErrorText(err));
|
||||
}
|
||||
|
||||
function isIncompleteFinalPreviewPrefix(previewText: string, finalText: string): boolean {
|
||||
const preview = previewText.trimEnd();
|
||||
const final = finalText.trimEnd();
|
||||
return preview.length > 0 && preview.length < final.length && final.startsWith(preview);
|
||||
}
|
||||
|
||||
export type LaneName = "answer" | "reasoning";
|
||||
|
||||
export type DraftLaneState = {
|
||||
|
|
@ -232,6 +238,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
|||
lane: DraftLaneState;
|
||||
finalTextAlreadyLanded: boolean;
|
||||
retainAlternatePreviewOnMissingTarget: boolean;
|
||||
targetPreviewText: string;
|
||||
}): Promise<PreviewEditResult> => {
|
||||
try {
|
||||
await params.editPreview({
|
||||
|
|
@ -294,7 +301,14 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
|||
);
|
||||
return "fallback";
|
||||
}
|
||||
// Default: ambiguous error — prefer incomplete over duplicate
|
||||
if (isIncompleteFinalPreviewPrefix(args.targetPreviewText, args.text)) {
|
||||
params.log(
|
||||
`telegram: ${args.laneName} preview final edit failed and existing preview is an incomplete prefix; falling back to standard send (${String(err)})`,
|
||||
);
|
||||
return "fallback";
|
||||
}
|
||||
// Default: ambiguous error — retain when fallback may duplicate a final
|
||||
// edit that already landed or when the preview is not known-incomplete.
|
||||
params.log(
|
||||
`telegram: ${args.laneName} preview final edit failed with ambiguous error; keeping existing preview to avoid duplicate (${String(err)})`,
|
||||
);
|
||||
|
|
@ -324,6 +338,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
|||
messageId: number,
|
||||
finalTextAlreadyLanded: boolean,
|
||||
retainAlternatePreviewOnMissingTarget: boolean,
|
||||
targetPreviewText: string,
|
||||
) =>
|
||||
tryEditPreviewMessage({
|
||||
laneName,
|
||||
|
|
@ -335,6 +350,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
|||
lane,
|
||||
finalTextAlreadyLanded,
|
||||
retainAlternatePreviewOnMissingTarget,
|
||||
targetPreviewText,
|
||||
});
|
||||
const finalizePreview = (
|
||||
previewMessageId: number,
|
||||
|
|
@ -357,6 +373,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
|||
previewMessageId,
|
||||
finalTextAlreadyLanded,
|
||||
retainAlternatePreviewOnMissingTarget,
|
||||
currentPreviewText,
|
||||
);
|
||||
};
|
||||
if (!lane.stream) {
|
||||
|
|
|
|||
|
|
@ -231,7 +231,8 @@ describe("createLaneTextDeliverer", () => {
|
|||
|
||||
it("retains preview when an existing preview final edit fails with ambiguous error", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
// Plain Error with no error_code → ambiguous, prefer incomplete over duplicate
|
||||
// Plain Error with no error_code → ambiguous. Retain unless the preview is
|
||||
// known to be an incomplete prefix of the final text.
|
||||
harness.editPreview.mockRejectedValue(new Error("500: preview edit failed"));
|
||||
|
||||
await expectFinalPreviewRetained({
|
||||
|
|
@ -241,6 +242,20 @@ describe("createLaneTextDeliverer", () => {
|
|||
expect(harness.editPreview).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("falls back when an ambiguous final edit failure would leave an incomplete preview", async () => {
|
||||
const harness = createHarness({
|
||||
answerMessageId: 999,
|
||||
answerLastPartialText: "Hello fi",
|
||||
});
|
||||
harness.editPreview.mockRejectedValue(new Error("500: preview edit failed"));
|
||||
|
||||
await expectFinalEditFallbackToSend({
|
||||
harness,
|
||||
text: HELLO_FINAL,
|
||||
expectedLogSnippet: "preview is an incomplete prefix; falling back",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when Telegram reports the current final edit target missing", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
harness.editPreview.mockRejectedValue(new Error("400: Bad Request: message to edit not found"));
|
||||
|
|
@ -478,6 +493,29 @@ describe("createLaneTextDeliverer", () => {
|
|||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
|
||||
});
|
||||
|
||||
it("falls back when an archived preview ambiguous final edit would leave an incomplete prefix", async () => {
|
||||
const harness = createHarness();
|
||||
harness.archivedAnswerPreviews.push({
|
||||
messageId: 5555,
|
||||
textSnapshot: "Hello fi",
|
||||
deleteIfUnused: true,
|
||||
});
|
||||
harness.editPreview.mockRejectedValue(new Error("500: preview edit failed"));
|
||||
|
||||
await expectFinalEditFallbackToSend({
|
||||
harness,
|
||||
text: HELLO_FINAL,
|
||||
expectedLogSnippet: "preview is an incomplete prefix; falling back",
|
||||
});
|
||||
expect(harness.editPreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: 5555,
|
||||
text: HELLO_FINAL,
|
||||
}),
|
||||
);
|
||||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
|
||||
});
|
||||
|
||||
it("keeps the active preview when an archived final edit target is missing", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
seedArchivedAnswerPreview(harness);
|
||||
|
|
|
|||
|
|
@ -14586,7 +14586,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
},
|
||||
streaming: {
|
||||
label: "Telegram Streaming Mode",
|
||||
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are detected; run doctor --fix to migrate.',
|
||||
},
|
||||
"streaming.mode": {
|
||||
label: "Telegram Streaming Mode",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue