Skip to content

Commit e03fe1e

Browse files
authored
fix(telegram): reuse preview for long text finals (#77658)
* fix(telegram): reuse preview for long text finals * test(qa): cover long telegram finals * fix(qa): satisfy extension lint * fix(qa): keep telegram long final fixture to two chunks * test(telegram): cover three chunk finals * fix(telegram): force long final preview boundary
1 parent 3290cba commit e03fe1e

12 files changed

Lines changed: 671 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
7373
- Agents/tools: honor narrow runtime tool allowlists when constructing embedded-runner tool families and bundled MCP/LSP runtimes, so cron/subagent runs that request tools such as `update_plan`, `browser`, `x_search`, channel login tools, or `group:plugins` no longer start with missing tools or unrelated bootstrap work. (#77519, #77532)
7474
- Codex plugin: mirror the experimental upstream app-server protocol and format generated TypeScript before drift checks, keeping OpenClaw's `experimentalApi` bridge compatible with latest Codex while preserving formatter gates.
7575
- Telegram/media: derive no-caption inbound media placeholders from saved MIME metadata instead of the Telegram `photo` shape, so non-image and mixed attachments no longer reach the model as `<media:image>`. Fixes #69793. Thanks @aspalagin.
76+
- Telegram/streaming: reuse the active preview as the first chunk for long text finals, so multi-chunk replies no longer create a transient extra bubble that appears and then disappears. Thanks @vincentkoc.
7677
- Agents/cache: keep per-turn runtime context out of ordinary chat system prompts while still delivering hidden current-turn context, restoring prompt-cache reuse on chat continuations. Fixes #77431. Thanks @Udjin79.
7778
- Gateway/startup: include resolved thinking and fast-mode defaults in the `agent model` startup log line, defaulting unset startup thinking to `medium` without mixing in reasoning visibility.
7879
- Gateway/update: resolve local gateway probe auth from the installed config during post-update restart verification, so token/device-authenticated VPS gateways are not misreported as unhealthy port conflicts after a package swap. Thanks @vincentkoc.

docs/channels/telegram.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
344344
For text-only replies:
345345

346346
- short DM/group/topic previews: OpenClaw keeps the same preview message and performs a final edit in place, unless a visible non-preview message was sent after the preview appeared
347+
- long text finals that split into multiple Telegram messages reuse the existing preview as the first final chunk when possible, then send only the remaining chunks
347348
- previews followed by visible non-preview output: OpenClaw sends the completed reply as a fresh final message and cleans up the older preview, so the final answer appears after intermediate output
348349
- previews older than about one minute: OpenClaw sends the completed reply as a fresh final message and then cleans up the preview, so Telegram's visible timestamp reflects completion time instead of the preview creation time
349350

docs/concepts/qa-e2e-automation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ Scenarios (`extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime
232232
- `telegram-tools-compact-command`
233233
- `telegram-whoami-command`
234234
- `telegram-context-command`
235+
- `telegram-long-final-reuses-preview`
236+
- `telegram-long-final-three-chunks`
235237

236238
Output artifacts:
237239

extensions/diagnostics-otel/src/service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2236,6 +2236,8 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
22362236
return;
22372237
case "session.long_running":
22382238
case "session.stalled":
2239+
case "session.recovery.completed":
2240+
case "session.recovery.requested":
22392241
return;
22402242
case "session.stuck":
22412243
recordSessionStuck(evt);

extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ describe("telegram live qa runtime", () => {
333333
"telegram-context-command",
334334
"telegram-current-session-status-tool",
335335
"telegram-mentioned-message-reply",
336+
"telegram-long-final-reuses-preview",
337+
"telegram-long-final-three-chunks",
336338
"telegram-mention-gating",
337339
]);
338340
expect(scenarios.map((scenario) => scenario.id)).toEqual([
@@ -343,6 +345,8 @@ describe("telegram live qa runtime", () => {
343345
"telegram-context-command",
344346
"telegram-current-session-status-tool",
345347
"telegram-mentioned-message-reply",
348+
"telegram-long-final-reuses-preview",
349+
"telegram-long-final-three-chunks",
346350
"telegram-mention-gating",
347351
]);
348352
expect(
@@ -355,6 +359,25 @@ describe("telegram live qa runtime", () => {
355359
.find((scenario) => scenario.id === "telegram-mentioned-message-reply")
356360
?.buildRun("sut_bot").replyToLatestSutMessage,
357361
).toBe(true);
362+
expect(
363+
scenarios
364+
.find((scenario) => scenario.id === "telegram-long-final-reuses-preview")
365+
?.buildRun("sut_bot"),
366+
).toMatchObject({
367+
expectedJoinedSutTextIncludes: ["TELEGRAM-LONG-FINAL-BEGIN", "TELEGRAM-LONG-FINAL-END"],
368+
expectedSutMessageCount: 2,
369+
});
370+
expect(
371+
scenarios
372+
.find((scenario) => scenario.id === "telegram-long-final-three-chunks")
373+
?.buildRun("sut_bot"),
374+
).toMatchObject({
375+
expectedJoinedSutTextIncludes: [
376+
"TELEGRAM-LONG-FINAL-3CHUNK-BEGIN",
377+
"TELEGRAM-LONG-FINAL-3CHUNK-END",
378+
],
379+
expectedSutMessageCount: 3,
380+
});
358381
});
359382

360383
it("keeps bot-to-bot plain mentions out of the default Telegram live set", () => {
@@ -382,6 +405,160 @@ describe("telegram live qa runtime", () => {
382405
).toEqual(["allowlist-block", "top-level-reply-shape", "restart-resume"]);
383406
});
384407

408+
it("asserts long Telegram final replies reuse the streamed preview message", () => {
409+
expect(() =>
410+
__testing.assertTelegramScenarioMessageSet({
411+
expectedJoinedSutTextIncludes: ["TELEGRAM-LONG-FINAL-BEGIN", "TELEGRAM-LONG-FINAL-END"],
412+
expectedSutMessageCount: 2,
413+
groupId: "-100123",
414+
scenarioId: "telegram-long-final-reuses-preview",
415+
sutBotId: 99,
416+
observedMessages: [
417+
{
418+
updateId: 1,
419+
messageId: 10,
420+
chatId: -100123,
421+
senderId: 99,
422+
senderIsBot: true,
423+
scenarioId: "telegram-long-final-reuses-preview",
424+
scenarioTitle: "Telegram long final reuses the preview message",
425+
matchedScenario: true,
426+
text: "TELEGRAM-LONG-FINAL-BEGIN part one ",
427+
timestamp: 1_700_000_000_000,
428+
inlineButtons: [],
429+
mediaKinds: [],
430+
},
431+
{
432+
updateId: 2,
433+
messageId: 11,
434+
chatId: -100123,
435+
senderId: 99,
436+
senderIsBot: true,
437+
scenarioId: "telegram-long-final-reuses-preview",
438+
scenarioTitle: "Telegram long final reuses the preview message",
439+
matchedScenario: true,
440+
text: "part two TELEGRAM-LONG-FINAL-END",
441+
timestamp: 1_700_000_001_000,
442+
inlineButtons: [],
443+
mediaKinds: [],
444+
},
445+
],
446+
}),
447+
).not.toThrow();
448+
449+
expect(() =>
450+
__testing.assertTelegramScenarioMessageSet({
451+
expectedSutMessageCount: 2,
452+
groupId: "-100123",
453+
scenarioId: "telegram-long-final-reuses-preview",
454+
sutBotId: 99,
455+
observedMessages: [
456+
{
457+
updateId: 1,
458+
messageId: 10,
459+
chatId: -100123,
460+
senderId: 99,
461+
senderIsBot: true,
462+
scenarioId: "telegram-long-final-reuses-preview",
463+
scenarioTitle: "Telegram long final reuses the preview message",
464+
matchedScenario: true,
465+
text: "preview",
466+
timestamp: 1_700_000_000_000,
467+
inlineButtons: [],
468+
mediaKinds: [],
469+
},
470+
{
471+
updateId: 2,
472+
messageId: 11,
473+
chatId: -100123,
474+
senderId: 99,
475+
senderIsBot: true,
476+
scenarioId: "telegram-long-final-reuses-preview",
477+
scenarioTitle: "Telegram long final reuses the preview message",
478+
matchedScenario: true,
479+
text: "final chunk one",
480+
timestamp: 1_700_000_001_000,
481+
inlineButtons: [],
482+
mediaKinds: [],
483+
},
484+
{
485+
updateId: 3,
486+
messageId: 12,
487+
chatId: -100123,
488+
senderId: 99,
489+
senderIsBot: true,
490+
scenarioId: "telegram-long-final-reuses-preview",
491+
scenarioTitle: "Telegram long final reuses the preview message",
492+
matchedScenario: true,
493+
text: "final chunk two",
494+
timestamp: 1_700_000_002_000,
495+
inlineButtons: [],
496+
mediaKinds: [],
497+
},
498+
],
499+
}),
500+
).toThrow("expected 2 SUT message(s), observed 3");
501+
});
502+
503+
it("accepts legitimate three-chunk Telegram final replies", () => {
504+
expect(() =>
505+
__testing.assertTelegramScenarioMessageSet({
506+
expectedJoinedSutTextIncludes: [
507+
"TELEGRAM-LONG-FINAL-3CHUNK-BEGIN",
508+
"TELEGRAM-LONG-FINAL-3CHUNK-END",
509+
],
510+
expectedSutMessageCount: 3,
511+
groupId: "-100123",
512+
scenarioId: "telegram-long-final-three-chunks",
513+
sutBotId: 99,
514+
observedMessages: [
515+
{
516+
updateId: 1,
517+
messageId: 10,
518+
chatId: -100123,
519+
senderId: 99,
520+
senderIsBot: true,
521+
scenarioId: "telegram-long-final-three-chunks",
522+
scenarioTitle: "Telegram three-chunk final keeps only final chunks",
523+
matchedScenario: true,
524+
text: "TELEGRAM-LONG-FINAL-3CHUNK-BEGIN part one ",
525+
timestamp: 1_700_000_000_000,
526+
inlineButtons: [],
527+
mediaKinds: [],
528+
},
529+
{
530+
updateId: 2,
531+
messageId: 11,
532+
chatId: -100123,
533+
senderId: 99,
534+
senderIsBot: true,
535+
scenarioId: "telegram-long-final-three-chunks",
536+
scenarioTitle: "Telegram three-chunk final keeps only final chunks",
537+
matchedScenario: true,
538+
text: "part two ",
539+
timestamp: 1_700_000_001_000,
540+
inlineButtons: [],
541+
mediaKinds: [],
542+
},
543+
{
544+
updateId: 3,
545+
messageId: 12,
546+
chatId: -100123,
547+
senderId: 99,
548+
senderIsBot: true,
549+
scenarioId: "telegram-long-final-three-chunks",
550+
scenarioTitle: "Telegram three-chunk final keeps only final chunks",
551+
matchedScenario: true,
552+
text: "part three TELEGRAM-LONG-FINAL-3CHUNK-END",
553+
timestamp: 1_700_000_002_000,
554+
inlineButtons: [],
555+
mediaKinds: [],
556+
},
557+
],
558+
}),
559+
).not.toThrow();
560+
});
561+
385562
it("matches scenario replies by thread or exact marker", () => {
386563
expect(
387564
__testing.matchesTelegramScenarioReply({

0 commit comments

Comments
 (0)