From 868ba16d845fdf0c677427a876a18be0ac6a3f75 Mon Sep 17 00:00:00 2001 From: yarnb Date: Sun, 26 Apr 2026 23:50:53 +0800 Subject: [PATCH] fix: populate SenderId on inbound MsgContext for multi-user routing OpenClaw core's `buildInboundUserContextPrefix` reads `ctx.SenderId` to populate the `sender_id` field of the inbound `Conversation info (untrusted metadata)` block. Memory plugins like `openclaw-honcho` parse that block to route per-sender peer state. Currently `weixinMessageToMsgContext` only sets `From: from_user_id` and leaves `SenderId` undefined, so the metadata block omits `sender_id`. Downstream plugins that depend on it (honcho `extractSenderId`, etc.) collapse all senders into a single peer, breaking multi-user isolation. This change: - Adds optional `SenderId?: string` to `WeixinMsgContext` with a doc comment explaining the contract with OpenClaw core. - Sets `SenderId: from_user_id` alongside `From` in `weixinMessageToMsgContext`. - Adds two test cases: one that asserts `SenderId` matches `from_user_id` for a normal message, and one that verifies the empty-string fallback when `from_user_id` is missing. For comparison, both `@wecom/wecom-openclaw-plugin` and `@larksuite/openclaw-lark` already populate `SenderId` on inbound; this brings weixin in line. --- src/messaging/inbound.test.ts | 10 ++++++++++ src/messaging/inbound.ts | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/src/messaging/inbound.test.ts b/src/messaging/inbound.test.ts index 8c507f7..df7d668 100644 --- a/src/messaging/inbound.test.ts +++ b/src/messaging/inbound.test.ts @@ -75,11 +75,21 @@ describe("weixinMessageToMsgContext", () => { expect(ctx.Timestamp).toBe(1700000000000); }); + it("populates SenderId from from_user_id so OpenClaw core can route per-sender peer state", () => { + // Without SenderId, downstream memory plugins (honcho, etc.) collapse all senders + // into a single peer because `buildInboundUserContextPrefix` reads `ctx.SenderId` + // when emitting the inbound `Conversation info` metadata block. Multi-user setups + // depend on this field being populated. + const ctx = weixinMessageToMsgContext(baseMsg, "account1"); + expect(ctx.SenderId).toBe("user123"); + }); + it("handles missing from_user_id", () => { const msg: WeixinMessage = { item_list: [] }; const ctx = weixinMessageToMsgContext(msg, "acc"); expect(ctx.From).toBe(""); expect(ctx.To).toBe(""); + expect(ctx.SenderId).toBe(""); }); it("handles empty item_list", () => { diff --git a/src/messaging/inbound.ts b/src/messaging/inbound.ts index fc25ab8..1632cee 100644 --- a/src/messaging/inbound.ts +++ b/src/messaging/inbound.ts @@ -147,6 +147,13 @@ export type WeixinMsgContext = { Timestamp?: number; Provider: "openclaw-weixin"; ChatType: "direct"; + /** + * The sender's wxid. Read by OpenClaw core's `buildInboundUserContextPrefix` to populate + * the `sender_id` field of the inbound `Conversation info` metadata block, which downstream + * memory plugins (e.g. honcho) use to route per-sender peer state. Without this, multi-user + * scenarios collapse all senders into a single peer. + */ + SenderId?: string; /** Set by monitor after resolveAgentRoute so dispatchReplyFromConfig uses the correct session. */ SessionKey?: string; context_token?: string; @@ -231,6 +238,7 @@ export function weixinMessageToMsgContext( OriginatingChannel: "openclaw-weixin", OriginatingTo: from_user_id, MessageSid: generateMessageSid(), + SenderId: from_user_id, Timestamp: msg.create_time_ms, Provider: "openclaw-weixin", ChatType: "direct",