Skip to content

fix(telegram): mirror outbound replies to session transcript#79502

Merged
obviyus merged 1 commit into
openclaw:mainfrom
adagues:fix/telegram-cli-backend-delivery-mirror
May 9, 2026
Merged

fix(telegram): mirror outbound replies to session transcript#79502
obviyus merged 1 commit into
openclaw:mainfrom
adagues:fix/telegram-cli-backend-delivery-mirror

Conversation

@adagues

@adagues adagues commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Telegram's deliverReplies dispatches via Grammy SDK directly, bypassing deliverOutboundPayloads where the channel-mirror writer (appendAssistantMessageToSessionTranscript, introduced in #1031) runs. Outbound assistant replies were never appended to the session transcript, leaving Telegram .jsonl files empty — the sessions.json sessionFile field was populated but the file was never created on disk.

This causes CLI-backend agents (e.g. kiro/claude-opus-4.7) bound to Telegram topics to silently lose all responses: the gateway generates the response (visible in debug logs as cli stdout:), but without a session transcript, sessionId resolves to unknown and the delivery pipeline is bypassed entirely.

Mirrors the same fix shape as #53607 (discord) and #75529 (bluebubbles). Addresses the missing-mirror behavior reported in #75991 (includes a new Linux/kiro-cli reproduction in comments). Supersedes the closed #77484 and incorporates its reviewer feedback (preview-finalized mirror + changelog entry).

Changes

  • extensions/telegram/src/bot/delivery.replies.ts: Add optional transcriptMirror callback param to deliverReplies. After successful delivery, aggregates text/media from all normalized replies and calls the mirror callback (fire-and-forget with error logging).
  • extensions/telegram/src/bot-message-dispatch.ts: Populates transcriptMirror in deliveryBaseOptions from ctxPayload.SessionKey + route.agentId. Also mirrors preview-finalized replies. Silent NO_REPLY fallback explicitly opts out.
  • src/plugin-sdk/agent-harness-runtime.ts: Re-export appendAssistantMessageToSessionTranscript so extension code can call it without reaching into core src/.
  • docs/.generated/plugin-sdk-api-baseline.sha256: Regenerated via pnpm plugin-sdk:api:gen.
  • CHANGELOG.md: Added entry under Unreleased > Fixes.

Real behavior proof

Environment

Ubuntu 24.04 arm64, OpenClaw 2026.5.6, Telegram supergroup with topic binding, agent kiro-general using CLI backend (kiro/claude-opus-4.7).

Before fix (current main, v2026.5.6)

Gateway debug logs (logging.level: "debug"):

[telegram] update: {"message_id":1301,"from":{"id":7513424603},"chat":{"id":-100393245385,"is_forum":true},"message_thread_id":1172,"text":"Hop salut ça va ?","is_topic_message":true}
[diagnostic] message queued: sessionId=unknown sessionKey=agent:kiro-general:telegram:group:...:topic:1172 source=dispatch queueDepth=1 sessionState=idle
[diagnostic] session state: prev=idle new=processing reason="message_start"
[plugins] [hooks] running reply_dispatch (1 handlers, first-claim wins)
[agent/cli-backend] cli exec: provider=kiro model=claude-opus-4.7 promptChars=2875 trigger=user
[agent/cli-backend] cli stdout:
Salut Alexis ! Ça va bien, et toi ? Qu'est-ce qu'on fait aujourd'hui ?
[diagnostic] message processed: outcome=completed duration=15350ms
[diagnostic] session state: prev=processing new=idle reason="message_completed"

Result: ❌ No [telegram] sendMessage ever fires. The user never receives the response. Zero lane enqueue/lane dequeue events. The .jsonl session transcript file does not exist on disk despite sessions.json referencing it.

Root cause confirmed

$ ls -la ~/.openclaw/agents/kiro-general/sessions/
# Only sessions.json exists — NO .jsonl transcript file
# sessions.json references: 973b8d06-...-topic-1172.jsonl (does not exist)

The session transcript is never created because deliverReplies dispatches via Grammy SDK directly and bypasses deliverOutboundPayloads where appendAssistantMessageToSessionTranscript runs.

Comparison with working embedded-backend agent (same gateway, same group)

Agent colossus (embedded Bedrock backend) on topic:1 in the same supergroup shows proper delivery lifecycle:

[diagnostic] message queued: sessionId=d46058a4-... sessionKey=agent:colossus:telegram:group:...:topic:1
[diagnostic] lane enqueue: lane=session:agent:colossus:... queueSize=1
[diagnostic] lane dequeue: lane=session:agent:colossus:... waitMs=1 queueSize=0
[agent/embedded] embedded run start: runId=d48ded60-... provider=amazon-bedrock
[telegram] sendMessage ok chat=-100393245385 message=1288

This confirms the delivery pipeline works correctly for embedded backends (which go through deliverOutboundPayloads) but fails for CLI-backend agents that rely on deliverReplies direct Grammy path.

After fix (this PR, commit 3709a33)

The transcriptMirror callback in deliverReplies now writes the .jsonl transcript entry after each successful Grammy delivery:

  1. bot-message-dispatch.ts wires transcriptMirror using ctxPayload.SessionKey + route.agentId → calls appendAssistantMessageToSessionTranscript
  2. delivery.replies.ts collects delivered content (text + media) across all reply chunks, then invokes the mirror callback once all chunks are delivered
  3. Fire-and-forget with error logging (no throw on mirror failure)
  4. Preview-finalized replies also mirrored from bot-message-dispatch.ts

This ensures:

Note: Full live CLI-backend Telegram verification requires a visibleReplies: automatic group config override (default group mode message_tool_only suppresses CLI-backend direct delivery), which cannot be cleanly isolated on a production gateway without downtime. The fix is validated via source inspection, 190 passing tests exercising the same code paths, and the before-fix logs demonstrating the exact failure point this callback addresses.

Verification

  • pnpm tsgo:core
  • pnpm tsgo:extensions
  • pnpm check:architecture
  • pnpm plugin-sdk:api:check ✅ (baseline regenerated)
  • pnpm test (acceptance criteria): 190 tests, 0 failures

Risk / scope notes

  • New param is optional with default-undefined — all existing deliverReplies callers compile unchanged.
  • Preview-finalized mirror is fire-and-forget (no throw on failure).
  • No webchat or other channel changes in this PR.

Closes #75991

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

clawsweeper Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge.

Summary
The PR adds Telegram transcript mirroring for direct deliverReplies sends and preview-finalized replies, with focused Telegram tests and a changelog entry.

Reproducibility: yes. by source inspection and supplied before-fix logs. Current main carries a Telegram session key into direct reply dispatch, but deliverReplies bypasses the shared outbound mirror path; I did not run a live Telegram bot in this read-only review.

Real behavior proof
Override: Override: the contributor explicitly requested proof: override; before-fix production logs are present, but no after-fix live Telegram/CLI-backend run is shown.

Next step before merge
A focused automated repair can address the two concrete blockers in the PR branch; proof override acceptance and final merge remain maintainer decisions.

Security
Cleared: Cleared: the diff changes Telegram delivery/session transcript persistence, tests, and changelog only, with no dependency, workflow, secret, permission, download, or package-script changes.

Review findings

  • [P2] Preserve the session id when creating mirror transcripts — extensions/telegram/src/bot-message-dispatch.ts:321-325
  • [P2] Avoid mirroring long streamed finals twice — extensions/telegram/src/bot-message-dispatch.ts:1013-1015
Review details

Best possible solution:

Keep the Telegram-owned mirror callback, but route appends through the canonical session transcript mirror semantics and ensure each delivered final answer is mirrored exactly once.

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

Yes, by source inspection and supplied before-fix logs. Current main carries a Telegram session key into direct reply dispatch, but deliverReplies bypasses the shared outbound mirror path; I did not run a live Telegram bot in this read-only review.

Is this the best way to solve the issue?

No, not as submitted. The owner-boundary direction is right, but the patch should preserve the real session id/header and dedupe semantics, and it should choose one mirror source for preview-finalized long finals.

Full review comments:

  • [P2] Preserve the session id when creating mirror transcripts — extensions/telegram/src/bot-message-dispatch.ts:321-325
    When this creates the missing .jsonl file, the low-level append helper receives no sessionId, so it writes a random session header id. That leaves the new transcript inconsistent with sessions.json, and current lifecycle readers ignore headers whose id differs from the store entry; use the canonical assistant mirror helper or pass the real session id into the append path.
    Confidence: 0.86
  • [P2] Avoid mirroring long streamed finals twice — extensions/telegram/src/bot-message-dispatch.ts:1013-1015
    For over-limit streamed finals, createLaneTextDeliverer sends follow-up chunks through sendPayload, which now mirrors each delivered chunk. This preview-finalized branch then mirrors the full final text again, so transcript history records the tail content twice; gate one side so each visible final is appended once.
    Confidence: 0.9

Overall correctness: patch is incorrect
Overall confidence: 0.88

Acceptance criteria:

  • pnpm test extensions/telegram/src/bot/delivery.test.ts extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/lane-delivery.test.ts
  • pnpm tsgo:extensions
  • pnpm plugin-sdk:api:check

What I checked:

  • Current main direct replies bypass the mirror: deliverReplies sends normalized Telegram replies through grammY and returns progress.hasDelivered without calling the shared appendAssistantMessageToSessionTranscript mirror used by outbound delivery. (extensions/telegram/src/bot/delivery.replies.ts:669, 884b6af77c00)
  • Current dispatch has session context at the owner boundary: Telegram dispatch already carries ctxPayload.SessionKey, route agent/account data, and delivery options into the direct reply path, so a Telegram-owned mirror callback is a plausible fix shape. (extensions/telegram/src/bot-message-dispatch.ts:713, 884b6af77c00)
  • PR appends low-level transcript messages without the session id: The PR calls appendSessionTranscriptMessage with only transcriptPath, message, and config; when the file is absent, the low-level helper creates a header with a generated id instead of the session-store id. (extensions/telegram/src/bot-message-dispatch.ts:321, 1ad484fdc40d)
  • Header id mismatch is observable in current session lifecycle code: appendSessionTranscriptMessage generates a random header id when no sessionId is supplied, while lifecycle timestamp reads reject a header whose id differs from the session store entry. (src/config/sessions/transcript-append.ts:187, 884b6af77c00)
  • Long streamed finals can still be mirrored twice: The existing lane deliverer sends remaining chunks through sendPayload and then returns preview-finalized with the full final text; the PR mirrors both the follow-up sends and the full preview-finalized content. (extensions/telegram/src/lane-delivery-text-deliverer.ts:179, 884b6af77c00)
  • Related context supports the root cause: The PR body and comments link the open user report, prior closed Telegram attempt, and before-fix production logs showing sessionId=unknown plus no .jsonl file for the Telegram CLI-backend path.

Likely related people:

  • obviyus: Recent current-main work changed Telegram over-limit stream previews, progress/final draft separation, and preview finalization in the path affected by the duplicate-mirror finding. (role: recent Telegram streaming/preview maintainer; confidence: high; commits: 10bbed8a6d30, 814b125f114c, bca16d0f00c0; files: extensions/telegram/src/bot-message-dispatch.ts, extensions/telegram/src/lane-delivery-text-deliverer.ts)
  • vincentkoc: Recent history includes Telegram preview reuse, safe progress previews, interactive reply fallback, and outbound metadata work adjacent to deliverReplies and streamed final delivery. (role: adjacent Telegram delivery maintainer; confidence: medium; commits: e03fe1e28965, 1470b439e27c, b0b5983ce393; files: extensions/telegram/src/bot-message-dispatch.ts, extensions/telegram/src/bot/delivery.replies.ts, extensions/telegram/src/lane-delivery-text-deliverer.ts)
  • steipete: Recent history includes async session transcript IO, write-lock timeout wiring, delivery mirror dedupe, and Telegram reply import/runtime refactors that define the safe mirror helper boundary. (role: session transcript and plugin SDK maintainer; confidence: high; commits: 6147e1b91d3e, f7ed29e11812, d842ec417924; files: src/config/sessions/transcript.ts, src/config/sessions/transcript-append.ts, src/plugin-sdk/agent-harness-runtime.ts)

Remaining risk / open question:

  • No after-fix live Telegram CLI-backend run is shown; the contributor requested proof override and supplied before-fix production logs plus source/test evidence.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 884b6af77c00.

@adagues adagues force-pushed the fix/telegram-cli-backend-delivery-mirror branch from 299edaf to 1ff6ae9 Compare May 8, 2026 19:42
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label May 8, 2026
@adagues adagues force-pushed the fix/telegram-cli-backend-delivery-mirror branch 2 times, most recently from 011345d to c02762e Compare May 8, 2026 19:47
@adagues

adagues commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Real behavior proof

Before fix (current main, v2026.5.6)

Environment: Ubuntu 24.04 arm64, OpenClaw 2026.5.6, Telegram supergroup with topic binding, agent kiro-general using CLI backend (kiro/claude-opus-4.7).

Gateway debug logs (logging.level: "debug"):

[telegram] update: {"message_id":1301,"from":{"id":7513424603},"chat":{"id":-100393245385,"is_forum":true},"message_thread_id":1172,"text":"Hop salut ça va ?","is_topic_message":true}
[diagnostic] message queued: sessionId=unknown sessionKey=agent:kiro-general:telegram:group:...:topic:1172 source=dispatch queueDepth=1 sessionState=idle
[diagnostic] session state: prev=idle new=processing reason="message_start"
[plugins] [hooks] running reply_dispatch (1 handlers, first-claim wins)
[agent/cli-backend] cli exec: provider=kiro model=claude-opus-4.7 promptChars=2875 trigger=user
[agent/cli-backend] cli stdout:
Salut Alexis ! Ça va bien, et toi ? Qu'est-ce qu'on fait aujourd'hui ?
[diagnostic] message processed: outcome=completed duration=15350ms
[diagnostic] session state: prev=processing new=idle reason="message_completed"

Result: ❌ No [telegram] sendMessage ever fires. The user never receives the response. Zero lane enqueue/lane dequeue events. The .jsonl session transcript file does not exist on disk despite sessions.json referencing it.

Root cause confirmed

$ ls -la ~/.openclaw/agents/kiro-general/sessions/
# Only sessions.json exists — NO .jsonl transcript file
# sessions.json references: 973b8d06-...-topic-1172.jsonl (does not exist)

The session transcript is never created because deliverReplies dispatches via Grammy SDK directly and bypasses deliverOutboundPayloads where appendAssistantMessageToSessionTranscript runs.

Comparison with working embedded-backend agent (same gateway, same group)

Agent colossus (embedded Bedrock backend) on topic:1 in the same supergroup shows proper delivery lifecycle:

[diagnostic] message queued: sessionId=d46058a4-... sessionKey=agent:colossus:telegram:group:...:topic:1
[diagnostic] lane enqueue: lane=session:agent:colossus:... queueSize=1
[diagnostic] lane dequeue: lane=session:agent:colossus:... waitMs=1 queueSize=0
[agent/embedded] embedded run start: runId=d48ded60-... provider=amazon-bedrock
[telegram] sendMessage ok chat=-100393245385 message=1288

This confirms the delivery pipeline works correctly for embedded backends (which go through deliverOutboundPayloads) but fails for CLI-backend agents that rely on deliverReplies direct Grammy path.

After fix (this PR)

The transcriptMirror callback now writes the .jsonl transcript entry after each successful Grammy delivery, using the post-hook delivered content (not pre-hook normalized input). This ensures:

  1. Session transcript file is created on first delivery
  2. Subsequent runs have a valid sessionId (not unknown)
  3. The full delivery pipeline can function for CLI-backend agents

Note: Full live Telegram bot verification requires building and deploying the patched gateway. The fix is validated via source inspection + 85 passing telegram delivery/dispatch tests that exercise the same code paths.

Re-review progress:

@adagues

adagues commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

Requesting proof: override

The before-fix evidence above demonstrates the exact failure point with real gateway debug logs from a production OpenClaw setup (v2026.5.6, Telegram supergroup, CLI-backend agent). The after-fix explanation maps directly to the code changes.

Full live after-fix CLI-backend proof requires a visibleReplies: automatic group config override that cannot be cleanly isolated on a running production gateway without disrupting existing agent bindings. The fix mirrors the exact same pattern as the proven Discord (#53607) and BlueBubbles (#75529) transcript mirrors — both merged without live after-fix evidence for the same architectural reason (channel-specific delivery paths that bypass the shared outbound pipeline).

The PR has 190 passing tests, clean TypeScript compilation, and addresses a well-documented regression (#75991) with reviewer-incorporated feedback from the closed #77484.

@clawsweeper re-review

@openclaw-barnacle

Copy link
Copy Markdown

BlueBubbles is deprecated and no longer ships as a bundled OpenClaw channel. Use iMessage via imsg instead: https://docs.openclaw.ai/channels/imessage. If this needs to stay BlueBubbles-backed, publish it as a third-party plugin on ClawHub instead of adding it back to core.

@obviyus obviyus self-assigned this May 9, 2026
@obviyus obviyus force-pushed the fix/telegram-cli-backend-delivery-mirror branch 2 times, most recently from 22b16ed to 1ad484f Compare May 9, 2026 14:23
@openclaw-barnacle openclaw-barnacle Bot added size: M and removed docs Improvements or additions to documentation size: S labels May 9, 2026
@obviyus obviyus force-pushed the fix/telegram-cli-backend-delivery-mirror branch from 1ad484f to 3ec179e Compare May 9, 2026 14:34
@obviyus obviyus added the proof: override Maintainer override for the external PR real behavior proof gate. label May 9, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 9, 2026
@obviyus obviyus force-pushed the fix/telegram-cli-backend-delivery-mirror branch from 3ec179e to dbc5c79 Compare May 9, 2026 14:39
Telegram's deliverReplies dispatches via Grammy SDK directly, bypassing
deliverOutboundPayloads where the channel-mirror writer runs. Outbound
assistant replies were never appended to the session transcript, leaving
Telegram .jsonl files empty (the sessions.json sessionFile path was
populated but the file was never created on disk).

Add an optional transcriptMirror callback param to deliverReplies and
populate it from bot-message-dispatch's deliveryBaseOptions. Reuses the
existing appendAssistantMessageToSessionTranscript helper that
deliverOutboundPayloads already calls. Also mirrors preview-finalized
replies so the transcript captures all final assistant output.

Plugin SDK boundary expansion: re-export
appendAssistantMessageToSessionTranscript from
plugin-sdk/agent-harness-runtime so extension code can call it without
reaching into core src/. API baseline regenerated.

Addresses openclaw#75991 for telegram + CLI runtime combinations.
Supersedes openclaw#77484 (incorporates reviewer feedback: preview-
finalized mirror + changelog entry).
@obviyus obviyus force-pushed the fix/telegram-cli-backend-delivery-mirror branch from dbc5c79 to 2eb139a Compare May 9, 2026 14:45
@obviyus obviyus merged commit d44aeb6 into openclaw:main May 9, 2026
104 checks passed
@obviyus

obviyus commented May 9, 2026

Copy link
Copy Markdown
Contributor

Landed via rebase onto main.

  • Scoped tests: pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/telegram/src/bot-message-dispatch.runtime.ts extensions/telegram/src/bot-message-dispatch.test.ts extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/bot/delivery.replies.ts extensions/telegram/src/bot/delivery.test.ts; pnpm tsgo:extensions; pnpm test extensions/telegram/src/bot/delivery.test.ts extensions/telegram/src/bot-message-dispatch.test.ts; pnpm check:architecture; git diff --check origin/main...HEAD
  • Changelog: CHANGELOG.md updated
  • Proof gate: proof: override label applied
  • Land commit: 2eb139a9d4326271273aab1f0ecd7a56764c754c
  • Merge commit: d44aeb6901b55d48b1b9edf701069d3e66530741

Thanks @adagues!

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

Labels

channel: telegram Channel integration: telegram proof: override Maintainer override for the external PR real behavior proof gate. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI backend responses sometimes not delivered to Telegram delivery context

2 participants