openclaw - 💡(How to fix) Fix [Feature] WhatsApp threadBindings.spawnSessions via reply-quote — drive thread-bound session routing using contextInfo.quotedMessage.stanzaId [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77080Fetched 2026-05-05 05:52:37
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
2
Timeline (top)
commented ×1

WhatsApp DM (and group) channels currently can't participate in the threadBindings.spawnSessions feature shipped in 2026.5.x because the framework requires native thread/topic primitives (Discord channels, Slack threads, Telegram topics). WhatsApp exposes a thread-like proxy — reply-quote — via contextInfo.quotedMessage.stanzaId on inbound messages, which OpenClaw already extracts but doesn't currently use for session routing. Proposing this be wired into the threadBindings flow so a reply-quoted inbound routes to the session that originally sent the quoted message.

Error Message

upsert; do not error.

Root Cause

WhatsApp DM (and group) channels currently can't participate in the threadBindings.spawnSessions feature shipped in 2026.5.x because the framework requires native thread/topic primitives (Discord channels, Slack threads, Telegram topics). WhatsApp exposes a thread-like proxy — reply-quote — via contextInfo.quotedMessage.stanzaId on inbound messages, which OpenClaw already extracts but doesn't currently use for session routing. Proposing this be wired into the threadBindings flow so a reply-quoted inbound routes to the session that originally sent the quoted message.

Fix Action

Fix / Workaround

The current workarounds are all unsatisfying:

Code Example

{
  "<outbound stanzaId>": {
    sessionKey: "session:<id>",
    agentId: "<id>",
    accountId: "default",
    boundAt: <ms>,
    lastActiveAt: <ms>,
  },
  ...
}

---

recordThreadBinding({
  stanzaId: sentMessage.id,
  sessionKey: ctx.sessionKey,
  agentId: ctx.agentId,
  accountId: ctx.accountId,
});

---

const quotedStanzaId = extractContextInfo(message)?.stanzaId;
if (quotedStanzaId && cfg.channels.whatsapp.threadBindings?.enabled) {
  const binding = await lookupThreadBinding(quotedStanzaId);
  if (binding && !isExpired(binding, cfg)) {
    overrideSessionKey(binding.sessionKey, binding.agentId);
    bumpLastActiveAt(binding);
  }
}
// else: existing DM routing (defaults to agent:<default>:main)

---

"channels": {
  "whatsapp": {
    "threadBindings": {
      "enabled": true,
      "idleHours": 2,
      "maxAgeHours": 24,
      "spawnSessions": true,
      "defaultSpawnContext": "isolated"
    }
  }
}

---

openclaw channels whatsapp threadbindings list   [--account <id>]
openclaw channels whatsapp threadbindings clear  --stanza-id <id>
openclaw channels whatsapp threadbindings expire [--idle | --max-age]
RAW_BUFFERClick to expand / collapse

Summary

WhatsApp DM (and group) channels currently can't participate in the threadBindings.spawnSessions feature shipped in 2026.5.x because the framework requires native thread/topic primitives (Discord channels, Slack threads, Telegram topics). WhatsApp exposes a thread-like proxy — reply-quote — via contextInfo.quotedMessage.stanzaId on inbound messages, which OpenClaw already extracts but doesn't currently use for session routing. Proposing this be wired into the threadBindings flow so a reply-quoted inbound routes to the session that originally sent the quoted message.

Motivation

Recurring crons that target non-main sessions (isolated, current, or session:<custom-id>) can deliver outbound messages reliably via announce, but user replies always land in agent:<id>:main per WhatsApp DM routing. This breaks any pattern where a scheduled nudge and the user's response need to share session context — daily standups, recurring research workflows, scheduled reviews, debrief flows, etc.

The current workarounds are all unsatisfying:

  • Use sessionTarget: "main" + payload.kind: "systemEvent" and rely on the cron-event prompt path. Fragile — payload-loss issues like #73189 recur intermittently when the main session is busy / the cron-event prompt builder gets pre-empted by bare heartbeats.
  • Use isolated + announce and have the agent's main session detect reply content via heuristics. Works for distinctive content shapes (e.g. pasted dumps) but fails on ambiguous replies ("ok here you go").
  • Use isolated + announce plus an external state-marker file that main reads on every inbound. Works but couples cron and main via out-of-band state and isn't natively supported by the framework.

Related

  • #65300 (open) — "ACP thread-bound replies missing threadId on non-native-thread channels". Describes the same root primitive gap.
  • #13892 (open) — "cron sessionTarget support for specific session keys". Adjacent (already partially landed via session:<id> sessionTarget values).
  • #73189 (closed) and #75964 (closed) — illustrate the recurring-cron fragility on main + systemEvent that the proposed feature would obviate.

Proposed design

Mirror the existing Discord threadBindings shape, using reply-quote as the WhatsApp-equivalent of a thread.

Storage

A persistent binding store (sqlite or JSON) keyed by outbound stanzaId:

{
  "<outbound stanzaId>": {
    sessionKey: "session:<id>",
    agentId: "<id>",
    accountId: "default",
    boundAt: <ms>,
    lastActiveAt: <ms>,
  },
  ...
}

Pruned periodically by idleHours (no activity since lastActiveAt) and maxAgeHours (hard cap from boundAt).

Outbound hook

When any session-originated outbound WhatsApp message is sent (the existing deliverWebReply / outbound send path), record:

recordThreadBinding({
  stanzaId: sentMessage.id,
  sessionKey: ctx.sessionKey,
  agentId: ctx.agentId,
  accountId: ctx.accountId,
});

Single write per outbound. No-op when channels.whatsapp.threadBindings.spawnSessions === false.

Inbound hook

In the WhatsApp inbound processing path, before the existing session-target resolution:

const quotedStanzaId = extractContextInfo(message)?.stanzaId;
if (quotedStanzaId && cfg.channels.whatsapp.threadBindings?.enabled) {
  const binding = await lookupThreadBinding(quotedStanzaId);
  if (binding && !isExpired(binding, cfg)) {
    overrideSessionKey(binding.sessionKey, binding.agentId);
    bumpLastActiveAt(binding);
  }
}
// else: existing DM routing (defaults to agent:<default>:main)

If the binding lookup fails (unknown stanzaId, expired, or session has since been pruned by cron.sessionRetention), fall back to the current default routing and log a structured note.

Config schema

Mirror channels.discord.threadBindings:

"channels": {
  "whatsapp": {
    "threadBindings": {
      "enabled": true,
      "idleHours": 2,
      "maxAgeHours": 24,
      "spawnSessions": true,
      "defaultSpawnContext": "isolated"
    }
  }
}

CLI

A small admin surface for visibility/cleanup:

openclaw channels whatsapp threadbindings list   [--account <id>]
openclaw channels whatsapp threadbindings clear  --stanza-id <id>
openclaw channels whatsapp threadbindings expire [--idle | --max-age]

Tradeoffs

The user has to use WhatsApp's reply-quote feature for the routing to fire. Replies typed without quoting fall through to default routing. This is a real friction point but acceptable as a default because:

  1. WhatsApp users on mobile already understand long-press → reply.
  2. Non-quoted replies preserving today's behavior is a safer migration story than overriding routing on heuristics.
  3. An optional follow-up enhancement could maintain an "active conversation window" so subsequent unquoted replies within idleHours continue to route to the bound session — equivalent to how Slack thread-mode behaves once a user has replied in-thread. Recommend shipping deterministic-only first, adding the activity window later if it's needed.

Group chat scope

Same stanzaId → sessionKey mapping conceptually works in group chats, but the routing fallback (which session is "main" for a group binding) differs. Recommend shipping DM-only first and treating group support as a follow-up.

Edge cases

  • Self-quoting: user quotes their own earlier message — no binding entry, fall through to default. Correct behavior.
  • Multi-message nudges: cron sends N messages — each gets its own binding entry pointing at the same sessionKey. Replying to any of them routes correctly.
  • Pruned session: binding lookup succeeds but the target session was pruned by cron.sessionRetention. Fall back to default routing and emit a structured warning (thread-binding.session_missing) for observability.
  • Idempotency: same stanzaId recorded twice (e.g. via retry) → upsert; do not error.
  • Slash escape: /main (or equivalent) forces the next inbound to bypass thread-binding and route to default, for "this isn't about that thread" recovery.

Implementation estimate

Roughly mirrors the existing Discord thread-binding plumbing: storage layer + outbound hook + inbound override + config schema + TTL sweep + CLI. Probably ~200 LoC of new code, plus tests. Most of the routing plumbing already exists for Discord and only needs WhatsApp-side adapter wiring.

extent analysis

TL;DR

Implement WhatsApp thread bindings by utilizing the reply-quote feature to route user replies to the correct session.

Guidance

  • Introduce a persistent binding store to map outbound stanzaId to session keys, allowing for efficient lookup of session context on inbound messages.
  • Implement an outbound hook to record thread bindings when sending WhatsApp messages, and an inbound hook to override session key resolution based on quoted stanza IDs.
  • Configure the threadBindings feature with settings such as enabled, idleHours, and maxAgeHours to control binding expiration and session routing.
  • Develop a CLI for administrators to manage thread bindings, including listing, clearing, and expiring bindings.

Example

recordThreadBinding({
  stanzaId: sentMessage.id,
  sessionKey: ctx.sessionKey,
  agentId: ctx.agentId,
  accountId: ctx.accountId,
});

This example demonstrates how to record a thread binding when sending an outbound WhatsApp message.

Notes

The proposed implementation relies on users utilizing WhatsApp's reply-quote feature for routing to work correctly. Non-quoted replies will fall back to default routing. Additional features, such as maintaining an "active conversation window" for subsequent unquoted replies, can be considered for future enhancements.

Recommendation

Apply the proposed workaround by implementing WhatsApp thread bindings, as it addresses the current limitations and provides a more reliable solution for routing user replies to the correct session.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Feature] WhatsApp threadBindings.spawnSessions via reply-quote — drive thread-bound session routing using contextInfo.quotedMessage.stanzaId [1 comments, 2 participants]