Skip to content

fix(telegram): skip chat_window context for persistent DMs already in session transcript (#87566)#87626

Closed
draix wants to merge 1 commit into
openclaw:mainfrom
draix:fix/87566-telegram-dm-duplicate-context
Closed

fix(telegram): skip chat_window context for persistent DMs already in session transcript (#87566)#87626
draix wants to merge 1 commit into
openclaw:mainfrom
draix:fix/87566-telegram-dm-duplicate-context

Conversation

@draix

@draix draix commented May 28, 2026

Copy link
Copy Markdown

Fixes #87566

Summary

Telegram private DMs that route to a persistent OpenClaw session (e.g. agent:main:main) were receiving a duplicated Conversation context (untrusted, chronological, selected for current message) block on every inbound user turn. The block contains the last ~10 cached Telegram messages — but those messages are already in the persistent session transcript, so the model saw the same history twice and paid for it twice on every turn.

This change suppresses the chat_window prompt-context block when (and only when):

  1. The chat is a Telegram private DM — no group, no supergroup, no channel.
  2. The chat has no topic/thread id — DM-topics and forum topics still get the chat_window.
  3. The route resolves to a session with prior activity (readSessionUpdatedAt returned a timestamp). Fresh sessions still get the chat_window.

No new config knob is introduced; the new behaviour is default-correct for the issue's repro and matches the clawsweeper "narrow session-aware suppression" recommendation.

Root cause

extensions/telegram/src/bot-handlers.runtime.ts builds a chat_window entry from the Telegram message cache on every inbound message via buildPromptContextForMessagebuildTelegramConversationContext({ recentLimit: 10, replyTargetWindowSize: 2 }), then extensions/telegram/src/bot-message-context.session.ts unconditionally attaches it as supplemental.untrustedContext. The persistent session transcript already covers that history, so for 1:1 DMs the block is pure duplication.

The fix lands at the attachment site in bot-message-context.session.ts, where we already have:

  • isGroup (from the route classifier)
  • threadSpec.id (forum / DM topic id, if any)
  • previousTimestamp = sessionRuntime.readSessionUpdatedAt({ storePath, sessionKey: route.sessionKey }) — the per-session updated-at, which is exactly the "has this session already been written to?" signal we need to distinguish persistent-with-transcript from fresh.

The filtering logic lives in a tiny pure helper (bot-message-context.prompt-context-filter.ts) so it is unit-testable independent of the bot.

Minimal reproduction

A two-turn Telegram DM into a persistent session (agent:main:main):

Turn 1 (cached): #6792  User    : "Can you finish the deploy?"
Turn 1 (cached): #6793  OpenClaw: "Update is running in the background…"
Turn 2 (inbound now): #6795 User: "and the release notes?"

Before (every user turn duplicates recent history)

…
Conversation info (untrusted metadata): …
Sender (untrusted metadata): …
Conversation context (untrusted, chronological, selected for current message):
  #6792 Thu 2026-05-28 12:49 GMT+9 User    : Can you finish the deploy?
  #6793 Thu 2026-05-28 12:50 GMT+9 OpenClaw: Update is running in the background…
  #6795 Thu 2026-05-28 12:55 GMT+9 User    : and the release notes?
[/Conversation context]

and the release notes?

(Persistent session transcript already contains #6792 + #6793 → those two are now sent twice on every turn.)

After

…
Conversation info (untrusted metadata): …
Sender (untrusted metadata): …

and the release notes?

Reply-chain / quote / forwarded context is preserved separately via supplemental.quote, supplemental.forwarded, and extra.ReplyChain, so dropping the chat_window does not lose reply context.

Token-impact estimate

On long-lived 1:1 Telegram DMs the duplicated block grows linearly until it stabilises at recentLimit: 10 messages. With OpenClaw's typical 200–400-token envelope per cached message, that is ~2–4k extra tokens per inbound turn appended to user content (which also defeats prefix caching), in line with @piazzatron's "~5x token burn" comment on a 1:1 session that otherwise carries only a short transcript per turn. Group / topic / reply / fresh-session paths are unchanged.

Architecture affected

Telegram inbound flow:

on("message")
  → bot-handlers.runtime.ts: processMessageWithReplyChain
      → buildPromptContextForMessage  ← still builds chat_window unchanged
      → processMessage
          → bot-message-context.session.ts: buildTelegramChannelInboundEventContext
              ↳ resolveTelegramConversationRoute → route.sessionKey, mainSessionKey
              ↳ sessionRuntime.readSessionUpdatedAt({ storePath, sessionKey })  ← persistent-vs-fresh signal
              ↳ filterTelegramPromptContextForPersistentDm(promptContext, { isGroup, threadId, previousTimestampMs })  ← NEW
              ↳ supplemental.untrustedContext = effectivePromptContext
                  → channel-inbound prompt formatter → "Conversation context (…)"

The decision lives entirely inside the Telegram extension; the channel-inbound formatter, prompt assembly, and session transcript paths are untouched.

Behaviour matrix

Scenario chat_window Reason
Private DM, persistent session (#87566) ❌ off transcript already contains the history
Private DM, fresh session (first turn) ✅ on no transcript yet → keep recent cached msgs
Group / supergroup ✅ on groups don't share a single persistent main session
Forum / group topic (message_thread_id) ✅ on per-topic session window is still useful
Private DM with reply target ❌ off reply context is preserved via ReplyChain/quote

Tests

Two new test files exercise the fix; both fail without it and pass with it.

extensions/telegram/src/bot-message-context.prompt-context-filter.test.ts — 11 pure unit tests of the suppression decision and the entry filter (group vs private, topic vs no-topic, persistent vs fresh, mixed entry types, identity / no-op semantics, edge-case timestamps).

extensions/telegram/src/bot.test.ts — 5 new integration tests under describe("Telegram chat_window suppression for persistent DMs (#87566)"):

  1. Private DM, persistent session → no chat_window in UntrustedStructuredContext.
  2. Private DM, fresh session (no previousTimestampMs) → chat_window present.
  3. Group chat, persistent session → chat_window present.
  4. Private DM with reply target, persistent session → chat_window suppressed, but ReplyChain preserved.
  5. Group topic (message_thread_id), persistent session → chat_window present.

Acceptance criteria — node scripts/run-vitest.mjs output

extensions/telegram/src/bot.test.ts:

 ✓ extension-telegram ../../extensions/telegram/src/bot.test.ts (77 tests) 4513ms
 Test Files  1 passed (1)
      Tests  77 passed (77)

extensions/telegram/src/message-cache.test.ts:

 ✓ extension-telegram ../../extensions/telegram/src/message-cache.test.ts (16 tests) 14ms
 Test Files  1 passed (1)
      Tests  16 passed (16)

New filter unit tests:

 ✓ extension-telegram ../../extensions/telegram/src/bot-message-context.prompt-context-filter.test.ts (11 tests) 1ms
 Test Files  1 passed (1)
      Tests  11 passed (11)

Out of scope

  • This PR does not change dmHistoryLimit semantics; the issue notes that knob is on a separate code path. The new behaviour is the default and needs no config opt-in or opt-out.
  • Named-account DMs that route to a per-account persistent session (e.g. agent:main:telegram:atlas:direct:alice-shared) are treated identically to default-account DMs because the suppression decision uses previousTimestamp on the resolved sessionKey, not the literal mainSessionKey value.
  • The chat_window build itself in buildPromptContextForMessage is left intact (it is still cheap and used by groups/topics/fresh sessions); we only filter at the attachment site. This keeps the surgery minimal and the suppression decision in one place.

CHANGELOG

Added under ## 2026.5.28### Fixes.

… session transcript (openclaw#87566)

Telegram private DMs that route to a persistent OpenClaw session were
receiving a duplicated "Conversation context (untrusted, chronological,
selected for current message)" block on every inbound user turn. The
block contains the last ~10 cached Telegram messages, all of which are
already in the session transcript for a persistent DM session — so the
model sees the same history twice and burns extra tokens / cache writes
on every turn.

Fix: filter chat_window entries out of the Telegram supplemental
untrustedContext when ALL of the following hold:

  - The chat is a Telegram private DM (no group, no topic/thread).
  - The session has prior activity (readSessionUpdatedAt returned a
    timestamp), i.e. it's a real persistent transcript and not a
    first-touch fresh session.

The fix preserves:

  - Group / supergroup / channel chats.
  - Forum / topic messages (message_thread_id present).
  - Reply / quote / forward context (handled separately via
    supplemental.quote, supplemental.forwarded, and extra.ReplyChain).
  - Fresh / never-used persistent sessions (first inbound still gets
    chat_window so the model has the cached recent messages).

No new config knob is introduced; the new behavior is default-correct
for the issue reporter's repro and the clawsweeper review.

Fixes openclaw#87566
@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram size: L triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 28, 2026
@clawsweeper

clawsweeper Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed May 29, 2026, 1:09 AM ET / 05:09 UTC.

Summary
Review failed before ClawSweeper could summarize the requested change.

PR surface: Source +86, Tests +420, Docs +1. Total +507 across 6 files.

Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path.

Review metrics: none identified.

Merge readiness
Overall: 🌊 off-meta tidepool
Proof: 🌊 off-meta tidepool
Patch quality: 🌊 off-meta tidepool
Result: rating does not apply to this item.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] No close action taken because the review did not complete.

Maintainer options:

  1. Decide the mitigation before merge
    Retry the Codex review after fixing the execution failure.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] Review did not complete, so no work-lane recommendation was made.
Review details

Best possible solution:

Retry the Codex review after fixing the execution failure.

Do we have a high-confidence way to reproduce the issue?

Unclear. The review failed before ClawSweeper could establish a reproduction path.

Is this the best way to solve the issue?

Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction.

AGENTS.md: unclear because the file could not be read completely.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 5fb83af3e389.

Label changes

Label changes:

  • remove P2: Current review triage priority is none.
  • remove merge-risk: 🚨 session-state: Current PR review selected no merge-risk labels.

Label justifications:

  • rating: 🌊 off-meta tidepool: Overall readiness is 🌊 off-meta tidepool; proof is 🌊 off-meta tidepool and patch quality is 🌊 off-meta tidepool.
Evidence reviewed

PR surface:

Source +86, Tests +420, Docs +1. Total +507 across 6 files.

View PR surface stats
Area Files Added Removed Net
Source 3 87 1 +86
Tests 2 420 0 +420
Docs 1 1 0 +1
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 6 508 1 +507

What I checked:

  • failure reason: codex execution failed.
  • codex failure detail: Codex review failed for this PR with exit 1.
  • codex stdout: Per-item Codex failure; continuing with the rest of the shard.

Likely related people:

  • unknown: Codex failed before it could trace repository history. (role: review did not complete; confidence: low)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels May 28, 2026
@clawsweeper

clawsweeper Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg: 🎁 locked until real behavior proof passes.

Details
  • No creature or rarity is rolled until proof passes.
  • Eggs are collectible flavor only; they do not affect labels, ratings, merge decisions, or automation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. size: L triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram DMs duplicate recent conversation context despite persistent session transcript

2 participants