openclaw - 💡(How to fix) Fix [zalouser] Forward data.quote (reply-quote metadata) into agent context

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…

The zalouser plugin currently drops most of the Zalo reply-quote metadata (data.quote) when building inbound messages, surfacing only ownerId for the implicitMention heuristic. Downstream consumers (agent prompt via inbound-meta.ts) therefore never see reply_to_id / reply_to_body for Zalo, despite the platform providing it on every quoted message.

This blocks any agent feature that needs to correlate a user's reply to a specific prior bot message (e.g. approval flows where a quote-reply implicitly identifies which proposal is being approved).

Root Cause

The zalouser plugin currently drops most of the Zalo reply-quote metadata (data.quote) when building inbound messages, surfacing only ownerId for the implicitMention heuristic. Downstream consumers (agent prompt via inbound-meta.ts) therefore never see reply_to_id / reply_to_body for Zalo, despite the platform providing it on every quoted message.

This blocks any agent feature that needs to correlate a user's reply to a specific prior bot message (e.g. approval flows where a quote-reply implicitly identifies which proposal is being approved).

Fix Action

Fix / Workaround

Applied the patch above on OpenClaw 2026.5.19 (commit 29faac2), rebuilt with pnpm build, and observed the new fields flowing into the agent context on the next quote-reply from Zalo. Use case: a QC penalty approval pipeline where the boss reply-quotes a proposal DM with "ok"/"no" and an agent rule deterministically maps the quoted messageId to a pending proposal record.

Code Example

export type TQuote = {
  ownerId: string;
  cliMsgId: number;
  globalMsgId: number;   // ← matches the messageId returned by `message send --json`
  cliMsgType: number;
  ts: number;
  msg: string;           // ← body of the quoted message
  attach: string;
  fromD: string;
  ttl: number;
};

---

const quoteOwnerId =
  data.quote && typeof data.quote === "object"
    ? toNumberId((data.quote as { ownerId?: unknown }).ownerId)
    : "";

---

{
     "chat_id": "...",
     "message_id": "...",
     "sender_id": "...",
     "sender": "...",
     "timestamp": "...",
     "inbound_event_kind": "user_request"
   }

---

canResolveExplicitMention?: boolean;
   implicitMention?: boolean;
+  quotedGlobalMsgId?: string;
+  quotedOwnerId?: string;
+  quotedBody?: string;
   eventMessage?: ZaloEventMessage;
   raw: unknown;
 };

---

const quoteOwnerId =
     data.quote && typeof data.quote === "object"
       ? toNumberId((data.quote as { ownerId?: unknown }).ownerId)
       : "";
+  const quotedGlobalMsgId =
+    data.quote && typeof data.quote === "object"
+      ? toStringValue((data.quote as { globalMsgId?: unknown }).globalMsgId)
+      : "";
+  const quotedBody =
+    data.quote && typeof data.quote === "object"
+      ? toStringValue((data.quote as { msg?: unknown }).msg)
+      : "";
   const hasAnyMention = mentionIds.length > 0;

---

wasExplicitlyMentioned,
     implicitMention,
+    quotedGlobalMsgId: quotedGlobalMsgId || undefined,
+    quotedOwnerId: quoteOwnerId || undefined,
+    quotedBody: quotedBody || undefined,
     eventMessage,
     raw: message,
   };

---

extra: {
       BodyForCommands: commandBody,
       GroupSubject: isGroup ? groupName || undefined : undefined,
       GroupChannel: isGroup ? groupName || undefined : undefined,
       GroupMembers: isGroup ? groupMembers : undefined,
       WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : undefined,
       CommandAuthorized: commandAuthorized,
+      ReplyToId: message.quotedGlobalMsgId || undefined,
+      ReplyToBody: message.quotedBody || undefined,
+      ReplyToIsQuote: message.quotedGlobalMsgId ? true : undefined,
     },
RAW_BUFFERClick to expand / collapse

Summary

The zalouser plugin currently drops most of the Zalo reply-quote metadata (data.quote) when building inbound messages, surfacing only ownerId for the implicitMention heuristic. Downstream consumers (agent prompt via inbound-meta.ts) therefore never see reply_to_id / reply_to_body for Zalo, despite the platform providing it on every quoted message.

This blocks any agent feature that needs to correlate a user's reply to a specific prior bot message (e.g. approval flows where a quote-reply implicitly identifies which proposal is being approved).

Evidence

zca-js exposes the full quote object (node_modules/zca-js/dist/models/Message.d.ts:54):

export type TQuote = {
  ownerId: string;
  cliMsgId: number;
  globalMsgId: number;   // ← matches the messageId returned by `message send --json`
  cliMsgType: number;
  ts: number;
  msg: string;           // ← body of the quoted message
  attach: string;
  fromD: string;
  ttl: number;
};

In extensions/zalouser/src/zalo-js.ts:913-916, toInboundMessage() extracts only ownerId:

const quoteOwnerId =
  data.quote && typeof data.quote === "object"
    ? toNumberId((data.quote as { ownerId?: unknown }).ownerId)
    : "";

The returned ZaloInboundMessage (types.ts:34-51) has no field for globalMsgId or msg, so the information is lost before monitor.ts builds the ctxPayload for the agent turn.

Meanwhile src/auto-reply/reply/inbound-meta.ts already expects ctx.ReplyToId / ctx.ReplyToBody / ctx.ReplyToIsQuote and propagates them into the prompt — confirmed by Telegram and other channels that do populate them. Only zalouser is missing this.

Reproduction

  1. Bot in a Zalo DM sends message A with --json, capture messageId.
  2. User reply-quotes message A with text "ok".
  3. Agent receives a user_request containing only:
    {
      "chat_id": "...",
      "message_id": "...",
      "sender_id": "...",
      "sender": "...",
      "timestamp": "...",
      "inbound_event_kind": "user_request"
    }
    (No reply_to_id, no reply_to_body, no is_quote.)

Expected: agent context should include the quoted message id and body so the agent (or a deterministic rule) can identify what is being replied to.

Proposed fix

3 minimal-diff changes (all in extensions/zalouser/src/):

1. types.ts — add 3 optional fields to ZaloInboundMessage

   canResolveExplicitMention?: boolean;
   implicitMention?: boolean;
+  quotedGlobalMsgId?: string;
+  quotedOwnerId?: string;
+  quotedBody?: string;
   eventMessage?: ZaloEventMessage;
   raw: unknown;
 };

2. zalo-js.ts — populate them in toInboundMessage()

   const quoteOwnerId =
     data.quote && typeof data.quote === "object"
       ? toNumberId((data.quote as { ownerId?: unknown }).ownerId)
       : "";
+  const quotedGlobalMsgId =
+    data.quote && typeof data.quote === "object"
+      ? toStringValue((data.quote as { globalMsgId?: unknown }).globalMsgId)
+      : "";
+  const quotedBody =
+    data.quote && typeof data.quote === "object"
+      ? toStringValue((data.quote as { msg?: unknown }).msg)
+      : "";
   const hasAnyMention = mentionIds.length > 0;
     wasExplicitlyMentioned,
     implicitMention,
+    quotedGlobalMsgId: quotedGlobalMsgId || undefined,
+    quotedOwnerId: quoteOwnerId || undefined,
+    quotedBody: quotedBody || undefined,
     eventMessage,
     raw: message,
   };

3. monitor.ts — surface them via ctxPayload.extra (matching inbound-meta.ts keys)

     extra: {
       BodyForCommands: commandBody,
       GroupSubject: isGroup ? groupName || undefined : undefined,
       GroupChannel: isGroup ? groupName || undefined : undefined,
       GroupMembers: isGroup ? groupMembers : undefined,
       WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : undefined,
       CommandAuthorized: commandAuthorized,
+      ReplyToId: message.quotedGlobalMsgId || undefined,
+      ReplyToBody: message.quotedBody || undefined,
+      ReplyToIsQuote: message.quotedGlobalMsgId ? true : undefined,
     },

After this, inbound-meta.ts will automatically inject reply_to_id, reply_to_body, and is_quote into the agent prompt, on par with other channels.

Verified locally

Applied the patch above on OpenClaw 2026.5.19 (commit 29faac2), rebuilt with pnpm build, and observed the new fields flowing into the agent context on the next quote-reply from Zalo. Use case: a QC penalty approval pipeline where the boss reply-quotes a proposal DM with "ok"/"no" and an agent rule deterministically maps the quoted messageId to a pending proposal record.

Notes

  • quotedGlobalMsgId is the Zalo platform message id, which equals the messageId returned by openclaw message send --channel zalouser --json (data.deliveries[*].messageId / top-level messageId). This makes server-side correlation straightforward.
  • The implicitMention heuristic still uses quoteOwnerId unchanged.
  • Happy to send a PR if useful.

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 [zalouser] Forward data.quote (reply-quote metadata) into agent context