@clawling/clawchat-plugin-openclaw 2026.7.1 → 2026.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -207,6 +207,28 @@ function decodeJwtStringClaim(token, claim) {
207
207
  return "";
208
208
  }
209
209
  }
210
+ /**
211
+ * Resolve the bot-account userId, validated against the token's `sub` claim.
212
+ *
213
+ * The WS session authenticates with the access token, so the backend binds the
214
+ * session to the token's `sub` — that IS the bot's user id for this session.
215
+ * Unlike ownerUserId (explicit config wins over the `oid` claim), a configured
216
+ * userId that disagrees with `sub` is always wrong for the session this token
217
+ * creates, and it silently breaks self-echo detection: with the OWNER's id
218
+ * here, every owner message is dropped as the bot's own echo (2026-07-02
219
+ * clawnest incident — a provisioner wrote the owner id into the config). Token
220
+ * wins; the mismatch is logged. Also derives userId from `sub` when nothing is
221
+ * configured, mirroring the `oid` fallback above.
222
+ */
223
+ function resolveUserId(token, configured) {
224
+ const tokenSub = decodeJwtStringClaim(token, "sub");
225
+ if (!tokenSub)
226
+ return configured;
227
+ if (configured && configured !== tokenSub) {
228
+ console.warn(`clawchat userId mismatch: configured ${configured} does not equal the access token's sub claim ${tokenSub}; using the token value. A misconfigured userId (e.g. the owner's id) breaks self-echo detection and mention gating — fix the channel userId / CLAWCHAT_USER_ID.`);
229
+ }
230
+ return tokenSub;
231
+ }
210
232
  function readGroupMode(value) {
211
233
  return value === "mention" ? "mention" : "all";
212
234
  }
@@ -312,7 +334,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
312
334
  const mediaBaseUrl = readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
313
335
  const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
314
336
  const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
315
- const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
337
+ const userId = resolveUserId(token, readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV));
316
338
  const ownerUserId = readOptionalString(channel.ownerUserId) ||
317
339
  readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV) ||
318
340
  // Fall back to the access token's `oid` (owner) claim. A provisioner may
@@ -1771,14 +1771,29 @@ export async function startOpenclawClawlingGateway(params) {
1771
1771
  catch (err) {
1772
1772
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw skill-update: bundled skills dir not resolvable (using managed-only current): ${err instanceof Error ? err.message : String(err)}`);
1773
1773
  }
1774
- const sendOwnerSkillMessage = async (text) => {
1774
+ // A usr_... id is not a valid chat_id — member-backend rejects it with
1775
+ // "invalid conversation id" and msghub never acks the send. Proactive owner
1776
+ // messages must be addressed to the direct conversation recorded at
1777
+ // activation (parity with the hermes adapter's _owner_direct_chat_id).
1778
+ const resolveOwnerDirectConversationId = () => {
1775
1779
  const ownerUserId = account.ownerUserId?.trim();
1776
1780
  if (!ownerUserId)
1781
+ return null;
1782
+ const conversationId = store
1783
+ ?.getActivationConversation?.({ platform: "openclaw", accountId })
1784
+ ?.conversationId?.trim();
1785
+ return conversationId || null;
1786
+ };
1787
+ const sendOwnerSkillMessage = async (text) => {
1788
+ const conversationId = resolveOwnerDirectConversationId();
1789
+ if (!conversationId) {
1790
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw skill-update owner message skipped: no activation conversation recorded`);
1777
1791
  return;
1792
+ }
1778
1793
  await sendOpenclawClawlingText({
1779
1794
  client,
1780
1795
  account,
1781
- to: { chatId: ownerUserId, chatType: "direct" },
1796
+ to: { chatId: conversationId, chatType: "direct" },
1782
1797
  text,
1783
1798
  log,
1784
1799
  });
@@ -2408,16 +2423,16 @@ export async function startOpenclawClawlingGateway(params) {
2408
2423
  },
2409
2424
  });
2410
2425
  const forwardGroupApprovalSlashToOwner = async (turn) => {
2411
- const ownerUserId = account.ownerUserId?.trim();
2412
- if (!ownerUserId) {
2413
- log?.error?.(`[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=missing_owner_user_id chat_id=${turn.peer.id} msg=${turn.messageId}`);
2426
+ const ownerConversationId = resolveOwnerDirectConversationId();
2427
+ if (!ownerConversationId) {
2428
+ log?.error?.(`[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=missing_owner_conversation chat_id=${turn.peer.id} msg=${turn.messageId}`);
2414
2429
  return "skipped";
2415
2430
  }
2416
2431
  try {
2417
2432
  await sendOpenclawClawlingText({
2418
2433
  client,
2419
2434
  account,
2420
- to: { chatId: ownerUserId, chatType: "direct" },
2435
+ to: { chatId: ownerConversationId, chatType: "direct" },
2421
2436
  text: groupOwnerAttentionText(turn.peer.id, turn.rawBody),
2422
2437
  log,
2423
2438
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.7.1",
3
+ "version": "2026.7.2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
package/src/config.ts CHANGED
@@ -332,6 +332,30 @@ function decodeJwtStringClaim(token: string, claim: string): string {
332
332
  }
333
333
  }
334
334
 
335
+ /**
336
+ * Resolve the bot-account userId, validated against the token's `sub` claim.
337
+ *
338
+ * The WS session authenticates with the access token, so the backend binds the
339
+ * session to the token's `sub` — that IS the bot's user id for this session.
340
+ * Unlike ownerUserId (explicit config wins over the `oid` claim), a configured
341
+ * userId that disagrees with `sub` is always wrong for the session this token
342
+ * creates, and it silently breaks self-echo detection: with the OWNER's id
343
+ * here, every owner message is dropped as the bot's own echo (2026-07-02
344
+ * clawnest incident — a provisioner wrote the owner id into the config). Token
345
+ * wins; the mismatch is logged. Also derives userId from `sub` when nothing is
346
+ * configured, mirroring the `oid` fallback above.
347
+ */
348
+ function resolveUserId(token: string, configured: string): string {
349
+ const tokenSub = decodeJwtStringClaim(token, "sub");
350
+ if (!tokenSub) return configured;
351
+ if (configured && configured !== tokenSub) {
352
+ console.warn(
353
+ `clawchat userId mismatch: configured ${configured} does not equal the access token's sub claim ${tokenSub}; using the token value. A misconfigured userId (e.g. the owner's id) breaks self-echo detection and mention gating — fix the channel userId / CLAWCHAT_USER_ID.`,
354
+ );
355
+ }
356
+ return tokenSub;
357
+ }
358
+
335
359
  function readGroupMode(value: unknown): GroupMode {
336
360
  return value === "mention" ? "mention" : "all";
337
361
  }
@@ -465,7 +489,10 @@ export function resolveOpenclawClawlingAccount(
465
489
  readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
466
490
  const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
467
491
  const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
468
- const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
492
+ const userId = resolveUserId(
493
+ token,
494
+ readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV),
495
+ );
469
496
  const ownerUserId =
470
497
  readOptionalString(channel.ownerUserId) ||
471
498
  readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV) ||
package/src/runtime.ts CHANGED
@@ -2166,13 +2166,30 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2166
2166
  }`,
2167
2167
  );
2168
2168
  }
2169
- const sendOwnerSkillMessage = async (text: string): Promise<void> => {
2169
+ // A usr_... id is not a valid chat_id — member-backend rejects it with
2170
+ // "invalid conversation id" and msghub never acks the send. Proactive owner
2171
+ // messages must be addressed to the direct conversation recorded at
2172
+ // activation (parity with the hermes adapter's _owner_direct_chat_id).
2173
+ const resolveOwnerDirectConversationId = (): string | null => {
2170
2174
  const ownerUserId = account.ownerUserId?.trim();
2171
- if (!ownerUserId) return;
2175
+ if (!ownerUserId) return null;
2176
+ const conversationId = store
2177
+ ?.getActivationConversation?.({ platform: "openclaw", accountId })
2178
+ ?.conversationId?.trim();
2179
+ return conversationId || null;
2180
+ };
2181
+ const sendOwnerSkillMessage = async (text: string): Promise<void> => {
2182
+ const conversationId = resolveOwnerDirectConversationId();
2183
+ if (!conversationId) {
2184
+ log?.info?.(
2185
+ `[${accountId}] clawchat-plugin-openclaw skill-update owner message skipped: no activation conversation recorded`,
2186
+ );
2187
+ return;
2188
+ }
2172
2189
  await sendOpenclawClawlingText({
2173
2190
  client,
2174
2191
  account,
2175
- to: { chatId: ownerUserId, chatType: "direct" },
2192
+ to: { chatId: conversationId, chatType: "direct" },
2176
2193
  text,
2177
2194
  log,
2178
2195
  });
@@ -2886,10 +2903,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2886
2903
  });
2887
2904
 
2888
2905
  const forwardGroupApprovalSlashToOwner = async (turn: IngestTurnParams): Promise<IngestTurnResult> => {
2889
- const ownerUserId = account.ownerUserId?.trim();
2890
- if (!ownerUserId) {
2906
+ const ownerConversationId = resolveOwnerDirectConversationId();
2907
+ if (!ownerConversationId) {
2891
2908
  log?.error?.(
2892
- `[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=missing_owner_user_id chat_id=${turn.peer.id} msg=${turn.messageId}`,
2909
+ `[${accountId}] clawchat-plugin-openclaw group approval command suppressed reason=missing_owner_conversation chat_id=${turn.peer.id} msg=${turn.messageId}`,
2893
2910
  );
2894
2911
  return "skipped";
2895
2912
  }
@@ -2897,7 +2914,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2897
2914
  await sendOpenclawClawlingText({
2898
2915
  client,
2899
2916
  account,
2900
- to: { chatId: ownerUserId, chatType: "direct" },
2917
+ to: { chatId: ownerConversationId, chatType: "direct" },
2901
2918
  text: groupOwnerAttentionText(turn.peer.id, turn.rawBody),
2902
2919
  log,
2903
2920
  });