Skip to content

Conversation context assembled per-transport, not per-session: cross-channel reply binds to a stale proposal and executes it #95378

Description

@MaiDuy708

Version: OpenClaw 2026.6.8 (844f405)
Severity: High — causes the agent to execute a wrong/unrelated action with confidence


Title

Conversation context is assembled per-transport, not per-session: a reply arriving on one channel cannot see the immediately-preceding turn that arrived on another channel (same shared agent session) → agent binds the reply to a stale, unrelated proposal and executes it.

Summary

When a single user interacts with one shared agent session (agent:main:main) through two different ingresses — the iOS app (gateway-protocol client, sender id openclaw-ios) and Telegram (channel plugin) — each turn's prompt context is built from only the originating ingress's own message store. The shared session contains all turns, but the per-turn context does not unify them. Result: the agent answers correctly for the corrupted context it received, but that context is missing the actual turn the user is replying to.

Impact

The agent executed a state-mutating tool call (cron job edit) that the user never requested, because it bound an ambiguous confirmation to the last proposal visible in the Telegram context window instead of the trading-analysis turn the user was actually replying to (which came in via the iOS app). The same mechanism would fire on any tool (incl. higher-stakes ones) and is not an authorization/gating issue — the context itself is wrong.

Reproduction

  1. Use one shared agent session reachable from two ingresses (iOS app + Telegram).
  2. On ingress A (iOS app): send an image/message; the agent produces analysis ending in a question (e.g. "want me to set X?").
  3. On ingress B (Telegram), within the recency window: send a short ambiguous reply that refers to A's turn (e.g. "no need, I already closed it").
  4. Observe: the agent's context for the ingress-B turn does not contain A's turn. Its most-recent visible context is whatever was last on Telegram (here: an older, unanswered proposal). The agent binds the reply to that proposal and acts on it.

Expected

The reply on ingress B is interpreted against the genuinely most-recent turn of the shared session, regardless of which ingress produced it.

Actual

The reply is interpreted against the most-recent message of ingress B's own store only. The cross-ingress turn is invisible.

Root cause (traced in dist 2026.6.8)

Two independent gaps stack so the cross-ingress turn is invisible from both context paths:

  1. Per-channel recency block (Telegram). bot-B3dz_0yx.js builds the "Conversation context (selected for current message)" via mConversationContext({ cache: messageCache, chatId: msg.chat.id, recentLimit: 10, replyTargetWindowSize: 2 }). messageCache = createTelegramMessageCache({ scope: resolveTelegramMessageCacheScope(...) }), and resolveTelegramMessageCacheScope(storePath) returns ${storePath}.telegram-messages.json — a Telegram-only message cache. recentLimit: 10 and replyTargetWindowSize: 2 are hardcoded. Messages from the gateway-protocol/iOS ingress are never written to this cache, so they cannot appear in the block.

  2. Cached stable prefix (messages[]). The compiled prompt's messages[] is an oldest-anchored cached prefix (for prompt-cache reuse; observed cacheRead ≈ 149k). By design it excludes the freshest turns — those are expected to arrive via the per-channel recency block in (1). So a turn that is both too new for the cached prefix and from a different ingress (so absent from the Telegram block) is invisible from both paths.

  3. No config knob. There is no setting to unify context across ingresses: channels.telegram has no session/store/context config (telegram.session = null, agents.defaults.session = null); no promptContext/conversationContext/historyScope/recentLimit/crossChannel key exists in config. The behavior is hardcoded in dist.

This is the classic multichannel (siloed) vs omnichannel (unified) distinction: OpenClaw routes multiple ingresses to one shared session (good), but assembles the model's recent-conversation context per-ingress (the defect).

Suggested fix

Assemble the per-turn recent-conversation context from the shared session transcript (which already contains every ingress's turns), not from a per-ingress cache — i.e. a session-scoped (not chat-scoped) recency window, or merge per-ingress caches by the unified session/actor id before building the prompt. Reference pattern: a single canonical conversation store keyed by user/session identity, with all ingresses normalized into it and context assembled from it (e.g. AWS Bedrock AgentCore Memory uses an identical actor_id across channels).

Notes

  • recentLimit: 10 and replyTargetWindowSize: 2 being hardcoded also limits any short-term mitigation via config.
  • A contributing amplifier (not the cause): the turn was classified BLITZ (low-scrutiny "answer directly"), which reduced the chance the agent paused to disambiguate.

Filed from production logs (OpenClaw 2026.6.8). Personal data redacted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions