@clawling/clawchat-plugin-openclaw 2026.8.6-1 → 2026.8.14-1

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.
@@ -13,6 +13,7 @@ import { ClawlingApiError } from "./api-types.js";
13
13
  import { RefreshManager } from "./refresh-manager.js";
14
14
  import { runOpenclawClawlingLogin, } from "./login.runtime.js";
15
15
  import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, resolveOpenclawClawlingAccount, } from "./config.js";
16
+ import { isValidChatId } from "./ws-client.js";
16
17
  import { dispatchOpenclawClawlingInbound } from "./inbound.js";
17
18
  import { PendingConsentStore, cleanupTombstonedManagedSkills, handleOwnerConsentReply, managedSkillExists, readLocalSkillState, resolveBundledSkillsDir, resolveManagedSkillsDir, runSkillUpdateCheck, } from "./skill-update.js";
18
19
  import { fetchInboundMedia } from "./media-runtime.js";
@@ -321,6 +322,22 @@ function withClawChatSessionScope(cfg) {
321
322
  },
322
323
  };
323
324
  }
325
+ /**
326
+ * Synthetic envelope builder for the post-activation greeting turn.
327
+ *
328
+ * The fourth synthetic INBOUND envelope carrying a chat_id, alongside
329
+ * `buildPermissionResultEnvelope`, `buildAwarenessNoteEnvelope` and the
330
+ * reply-dispatcher's owner-direct path — and it carries the same invariant.
331
+ *
332
+ * `conversationId` MUST be a conversation idcode (`cnv_…`, `isValidChatId`),
333
+ * the one recorded at activation from `agents/connect`'s `conversation.id`.
334
+ * The agent's in-turn reply INHERITS this envelope's chat_id
335
+ * (src/inbound.ts → runtime.dispatch → src/reply-dispatcher.ts), and msghub
336
+ * resolves a chat_id only through member-backend — anything else comes back
337
+ * "invalid conversation id", so the greeting is refused at the outbound
338
+ * boundary after a full LLM turn has already been spent. Callers must check at
339
+ * the source and skip rather than hand an unroutable id down here.
340
+ */
324
341
  function buildActivationBootstrapEnvelope(params) {
325
342
  const text = buildActivationBootstrapText();
326
343
  const now = Date.now();
@@ -2845,6 +2862,18 @@ export async function startOpenclawClawlingGateway(params) {
2845
2862
  return;
2846
2863
  }
2847
2864
  const claimedBootstrap = bootstrap;
2865
+ // The claimed id is about to become the chat_id of a synthetic INBOUND
2866
+ // envelope, and the agent's in-turn reply inherits it. A statically
2867
+ // invalid one can never be delivered — and unlike a dead chat it can
2868
+ // never become valid either — so skip it here instead of buying a full
2869
+ // LLM turn whose answer the outbound boundary will refuse. Release the
2870
+ // claim (a later re-login can record a real conversation id) but do NOT
2871
+ // arm a retry: retrying an unroutable id would just loop.
2872
+ if (!isValidChatId(claimedBootstrap.conversationId)) {
2873
+ log?.error?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap skipped: chat_id="${claimedBootstrap.conversationId}" reason=invalid_chat_id`);
2874
+ releaseBootstrap();
2875
+ return;
2876
+ }
2848
2877
  claimedInFlight = true;
2849
2878
  incrementActivationBootstrapInFlight(accountId);
2850
2879
  bootstrapGreetingDelivered = false;
@@ -131,6 +131,28 @@ export const SERVER_REJECTION_TTL_MS = 600_000;
131
131
  * MUST stay identical to the hermes plugin's `CHAT_ID_PREFIX`.
132
132
  */
133
133
  export const CHAT_ID_PREFIX = "cnv_";
134
+ /**
135
+ * Total length of a conversation id: 3-char prefix + `_` + 26-char body.
136
+ *
137
+ * member-backend mints every id as `<prefix>_<26 Crockford base32 chars of a
138
+ * UUID v7>` and its validator refuses anything of another length outright, so
139
+ * the length is part of the contract, not a coincidence.
140
+ */
141
+ const CHAT_ID_LENGTH = 30;
142
+ /**
143
+ * Crockford base32 — the alphabet member-backend encodes the 128-bit id body
144
+ * with. `I`, `L`, `O` and `U` are deliberately absent (they read as `1`, `1`,
145
+ * `0` and `V`), and member-backend's decoder does NOT fold them in: it fails.
146
+ *
147
+ * Decoding is case-insensitive there, so both cases are accepted here. The
148
+ * 3-char prefix is NOT: it must be lowercase `a`..`z`, which `CHAT_ID_PREFIX`
149
+ * already pins.
150
+ */
151
+ const CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
152
+ const CROCKFORD_SYMBOLS = new Set([
153
+ ...CROCKFORD_ALPHABET,
154
+ ...CROCKFORD_ALPHABET.toLowerCase(),
155
+ ]);
134
156
  /**
135
157
  * Whether `chatId` can name a ClawChat conversation at all.
136
158
  *
@@ -138,9 +160,30 @@ export const CHAT_ID_PREFIX = "cnv_";
138
160
  * exists, is alive, or admits this sender. Those are server-side questions
139
161
  * answered by `message.error`; this one is answerable locally, and a frame that
140
162
  * fails it is always wrong to put on the wire.
163
+ *
164
+ * Mirrors member-backend's own id validator, which is the only authority on the
165
+ * shape. The prefix alone is NOT the rule: `cnv_…:thr_1`, a bare `cnv_`, and a
166
+ * `cnv_`-prefixed display name all pass a prefix test and are still answered
167
+ * with `code=400: invalid conversation id`.
168
+ *
169
+ * MUST stay equivalent to the hermes plugin's validator; the shared fixture
170
+ * `src/__fixtures__/permission-events/invalid-chat-id-outbound.json` pins the
171
+ * rule and the samples for both.
141
172
  */
142
173
  export function isValidChatId(chatId) {
143
- return typeof chatId === "string" && chatId.startsWith(CHAT_ID_PREFIX);
174
+ if (typeof chatId !== "string")
175
+ return false;
176
+ // Pins bytes 0-2 (the lowercase `cnv` prefix) and byte 3 (the `_`
177
+ // separator) in one check; the length pins the 26-char body.
178
+ if (chatId.length !== CHAT_ID_LENGTH)
179
+ return false;
180
+ if (!chatId.startsWith(CHAT_ID_PREFIX))
181
+ return false;
182
+ for (let i = CHAT_ID_PREFIX.length; i < CHAT_ID_LENGTH; i++) {
183
+ if (!CROCKFORD_SYMBOLS.has(chatId[i]))
184
+ return false;
185
+ }
186
+ return true;
144
187
  }
145
188
  export class ClawChatClient extends EventEmitter {
146
189
  opts;
@@ -374,7 +417,14 @@ export class ClawChatClient extends EventEmitter {
374
417
  // valid. Rejecting at this entry point is therefore sufficient: the frame
375
418
  // never reaches the reconnect queue, so no replay path can resurrect it.
376
419
  //
377
- // Frames with no chat_id (connect / ping / pong) are untouched.
420
+ // Frames with no chat_id (connect / ping / pong) are untouched. The test is
421
+ // `!== undefined`, NOT a truthiness check, and that asymmetry is the
422
+ // contract: a chat_id that is ABSENT means "this event has no conversation"
423
+ // and passes, while one that is PRESENT-but-unusable — explicit `null`,
424
+ // `""`, a malformed id — is a frame that meant to address a conversation
425
+ // and named none, so it is dropped. msghub agrees: it decodes a JSON null
426
+ // into the empty string and every business event requires a non-empty
427
+ // chat_id.
378
428
  if (env.chat_id !== undefined && !isValidChatId(env.chat_id))
379
429
  return;
380
430
  this.sendWire(JSON.stringify(env), { bypassReconnectQueue: env.event === EVENT.CONNECT });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.8.6-1",
3
+ "version": "2026.8.14-1",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -1,19 +1,28 @@
1
- ## What Kind Of Group This Is
1
+ ## What kind of group this is
2
2
 
3
- - Until more specific guidance is available, treat this as an ordinary social group in the owner's circle.
4
- - Group members may chat naturally, share updates, discuss ideas, coordinate timing, or move small plans forward.
5
- - Infer the group's tone from how members speak to each other instead of assuming it is a meeting, ticket queue, or task space.
3
+ - Until you know otherwise, treat this as an ordinary social group in the
4
+ owner's circle.
5
+ - Members may chat, share news, kick ideas around, or make small plans together.
6
+ - Read the group's tone from how members actually talk to each other. Do not
7
+ assume it is a meeting, a ticket queue, or a task board.
6
8
 
7
- ## How The Agent Should Participate
9
+ ## How to take part
8
10
 
9
- - Reply briefly and directly when mentioned, explicitly asked, or when you can add useful information.
10
- - Answer clear factual, background, or option-setting questions when you can.
11
- - When a multi-person discussion becomes scattered, lightly summarize the current topic, agreements, or open questions.
12
- - Otherwise, prefer listening; the default reply density for this group is low, and most messages do not need you.
11
+ - Reply briefly and directly when you are mentioned, asked a question outright,
12
+ or can add something the group does not already have.
13
+ - Answer factual or background questions, and help weigh options, when you can
14
+ do it clearly.
15
+ - When a discussion has scattered, a short recap of the topic, what has been
16
+ agreed, and what is still open can help. Offer it once; do not chair the
17
+ conversation.
18
+ - Otherwise, listen. Most messages here do not need you.
13
19
 
14
- ## What To Be Especially Careful About
20
+ ## What to be careful about here
15
21
 
16
- - Do not bring private details from direct chats, other groups, or owner memory about a member into this group.
17
- - Do not reply to other bots or agents in the group, even if they mention you.
18
- - Do not take public-facing action for the owner in or about this group unless the owner explicitly agrees.
19
- - Do not force casual conversation into task tracking or take over a topic when members have not asked for that.
22
+ - Do not bring private detail about a member into this group — not from a direct
23
+ chat, not from another group, not from the owner's memory.
24
+ - Do not reply to other bots or agents here, even when they mention you.
25
+ - Do not act publicly for the owner in or about this group unless they clearly
26
+ agreed.
27
+ - Do not turn casual conversation into task tracking, and do not take over a
28
+ topic nobody asked you to run.
@@ -1,27 +1,31 @@
1
- ## What You Should Aim For
1
+ ## How to be here
2
2
 
3
- - Be present in conversation with warmth and attention.
4
- - Treat people in the owner's circle as specific people, not as tasks to process:
5
- - Pick up on what mattered to them last time.
6
- - Continue relationships that have already been established.
7
- - Avoid making every interaction feel like it starts from zero.
8
- - Remember preferences the owner has already expressed, and follow them without making the owner repeat the same request.
3
+ - Be present with warmth and attention. These are specific people, not tasks to
4
+ process: pick up what mattered to them last time, continue the relationship
5
+ that already exists, and do not act as if every conversation starts from zero.
6
+ - Remember what the owner has already told you and act on it, rather than making
7
+ them say it again.
9
8
 
10
- ## Where You Have Room To Act
9
+ ## Where you can act on your own
11
10
 
12
- - In the current direct chat, respond naturally and maintain relationship context without asking the owner about every small step.
13
- - When someone asks a factual, contextual, option-setting, or organizing question, help clearly and concisely.
14
- - In group chats, listen by default; speak when addressed, explicitly asked, or when you can materially help move the discussion forward.
15
- - Build and maintain social memory for the people you meet while using that memory only in the appropriate context.
11
+ - In a direct chat, respond naturally and keep the relationship going without
12
+ checking in about every small thing.
13
+ - When someone asks for facts, background, or help weighing options, answer
14
+ clearly and briefly.
15
+ - In a group, listen by default. Speak when you are mentioned, asked directly,
16
+ or can genuinely move the discussion forward.
17
+ - Build up what you know about the people you meet — and use it only where it
18
+ belongs.
16
19
 
17
- ## What You Should Avoid
20
+ ## What to avoid
18
21
 
19
- - Do not take outward-facing action for the owner unless the owner explicitly agrees, for example:
20
- - Making promises or commitments.
21
- - Inviting people or adding them to groups.
22
- - Contacting someone.
23
- - Publishing something publicly.
24
- - Changing profile information.
25
- - Sending important messages.
26
- - Do not pretend to be the owner; when you do not know what the owner would think, say you are unsure or ask the person to wait for owner confirmation.
27
- - Do not bring private details from direct chats, other groups, or owner memory into contexts where they do not belong.
22
+ - Do not take outward-facing action on the owner's behalf unless they clearly
23
+ agreed: making promises or accepting arrangements, inviting people, joining or
24
+ adding people to groups, contacting someone, publishing publicly, changing
25
+ profile information, sending a message that matters.
26
+ - Do not pretend to know what the owner would think. When you are unsure, say
27
+ so, or ask the other person to wait for the owner.
28
+ - **Do not carry private detail across contexts.** What you learned in a direct
29
+ chat does not belong in a group; what one group said does not belong in
30
+ another; what is in the owner's memory does not belong in front of their
31
+ friends. This is the one mistake here that cannot be taken back.
package/src/runtime.ts CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  resolveOpenclawClawlingAccount,
41
41
  type ResolvedOpenclawClawlingAccount,
42
42
  } from "./config.ts";
43
- import type { ClawlingChatClient } from "./ws-client.ts";
43
+ import { isValidChatId, type ClawlingChatClient } from "./ws-client.ts";
44
44
  import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
45
45
  import {
46
46
  PendingConsentStore,
@@ -479,6 +479,22 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
479
479
  };
480
480
  }
481
481
 
482
+ /**
483
+ * Synthetic envelope builder for the post-activation greeting turn.
484
+ *
485
+ * The fourth synthetic INBOUND envelope carrying a chat_id, alongside
486
+ * `buildPermissionResultEnvelope`, `buildAwarenessNoteEnvelope` and the
487
+ * reply-dispatcher's owner-direct path — and it carries the same invariant.
488
+ *
489
+ * `conversationId` MUST be a conversation idcode (`cnv_…`, `isValidChatId`),
490
+ * the one recorded at activation from `agents/connect`'s `conversation.id`.
491
+ * The agent's in-turn reply INHERITS this envelope's chat_id
492
+ * (src/inbound.ts → runtime.dispatch → src/reply-dispatcher.ts), and msghub
493
+ * resolves a chat_id only through member-backend — anything else comes back
494
+ * "invalid conversation id", so the greeting is refused at the outbound
495
+ * boundary after a full LLM turn has already been spent. Callers must check at
496
+ * the source and skip rather than hand an unroutable id down here.
497
+ */
482
498
  function buildActivationBootstrapEnvelope(params: {
483
499
  account: ResolvedOpenclawClawlingAccount;
484
500
  conversationId: string;
@@ -3379,6 +3395,20 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
3379
3395
  return;
3380
3396
  }
3381
3397
  const claimedBootstrap = bootstrap;
3398
+ // The claimed id is about to become the chat_id of a synthetic INBOUND
3399
+ // envelope, and the agent's in-turn reply inherits it. A statically
3400
+ // invalid one can never be delivered — and unlike a dead chat it can
3401
+ // never become valid either — so skip it here instead of buying a full
3402
+ // LLM turn whose answer the outbound boundary will refuse. Release the
3403
+ // claim (a later re-login can record a real conversation id) but do NOT
3404
+ // arm a retry: retrying an unroutable id would just loop.
3405
+ if (!isValidChatId(claimedBootstrap.conversationId)) {
3406
+ log?.error?.(
3407
+ `[${accountId}] clawchat-plugin-openclaw activation bootstrap skipped: chat_id="${claimedBootstrap.conversationId}" reason=invalid_chat_id`,
3408
+ );
3409
+ releaseBootstrap();
3410
+ return;
3411
+ }
3382
3412
  claimedInFlight = true;
3383
3413
  incrementActivationBootstrapInFlight(accountId);
3384
3414
  bootstrapGreetingDelivered = false;
package/src/ws-client.ts CHANGED
@@ -202,6 +202,30 @@ export const SERVER_REJECTION_TTL_MS = 600_000;
202
202
  */
203
203
  export const CHAT_ID_PREFIX = "cnv_";
204
204
 
205
+ /**
206
+ * Total length of a conversation id: 3-char prefix + `_` + 26-char body.
207
+ *
208
+ * member-backend mints every id as `<prefix>_<26 Crockford base32 chars of a
209
+ * UUID v7>` and its validator refuses anything of another length outright, so
210
+ * the length is part of the contract, not a coincidence.
211
+ */
212
+ const CHAT_ID_LENGTH = 30;
213
+
214
+ /**
215
+ * Crockford base32 — the alphabet member-backend encodes the 128-bit id body
216
+ * with. `I`, `L`, `O` and `U` are deliberately absent (they read as `1`, `1`,
217
+ * `0` and `V`), and member-backend's decoder does NOT fold them in: it fails.
218
+ *
219
+ * Decoding is case-insensitive there, so both cases are accepted here. The
220
+ * 3-char prefix is NOT: it must be lowercase `a`..`z`, which `CHAT_ID_PREFIX`
221
+ * already pins.
222
+ */
223
+ const CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
224
+ const CROCKFORD_SYMBOLS: ReadonlySet<string> = new Set([
225
+ ...CROCKFORD_ALPHABET,
226
+ ...CROCKFORD_ALPHABET.toLowerCase(),
227
+ ]);
228
+
205
229
  /**
206
230
  * Whether `chatId` can name a ClawChat conversation at all.
207
231
  *
@@ -209,9 +233,26 @@ export const CHAT_ID_PREFIX = "cnv_";
209
233
  * exists, is alive, or admits this sender. Those are server-side questions
210
234
  * answered by `message.error`; this one is answerable locally, and a frame that
211
235
  * fails it is always wrong to put on the wire.
236
+ *
237
+ * Mirrors member-backend's own id validator, which is the only authority on the
238
+ * shape. The prefix alone is NOT the rule: `cnv_…:thr_1`, a bare `cnv_`, and a
239
+ * `cnv_`-prefixed display name all pass a prefix test and are still answered
240
+ * with `code=400: invalid conversation id`.
241
+ *
242
+ * MUST stay equivalent to the hermes plugin's validator; the shared fixture
243
+ * `src/__fixtures__/permission-events/invalid-chat-id-outbound.json` pins the
244
+ * rule and the samples for both.
212
245
  */
213
246
  export function isValidChatId(chatId: unknown): chatId is string {
214
- return typeof chatId === "string" && chatId.startsWith(CHAT_ID_PREFIX);
247
+ if (typeof chatId !== "string") return false;
248
+ // Pins bytes 0-2 (the lowercase `cnv` prefix) and byte 3 (the `_`
249
+ // separator) in one check; the length pins the 26-char body.
250
+ if (chatId.length !== CHAT_ID_LENGTH) return false;
251
+ if (!chatId.startsWith(CHAT_ID_PREFIX)) return false;
252
+ for (let i = CHAT_ID_PREFIX.length; i < CHAT_ID_LENGTH; i++) {
253
+ if (!CROCKFORD_SYMBOLS.has(chatId[i]!)) return false;
254
+ }
255
+ return true;
215
256
  }
216
257
 
217
258
  /**
@@ -468,7 +509,14 @@ export class ClawChatClient extends EventEmitter {
468
509
  // valid. Rejecting at this entry point is therefore sufficient: the frame
469
510
  // never reaches the reconnect queue, so no replay path can resurrect it.
470
511
  //
471
- // Frames with no chat_id (connect / ping / pong) are untouched.
512
+ // Frames with no chat_id (connect / ping / pong) are untouched. The test is
513
+ // `!== undefined`, NOT a truthiness check, and that asymmetry is the
514
+ // contract: a chat_id that is ABSENT means "this event has no conversation"
515
+ // and passes, while one that is PRESENT-but-unusable — explicit `null`,
516
+ // `""`, a malformed id — is a frame that meant to address a conversation
517
+ // and named none, so it is dropped. msghub agrees: it decodes a JSON null
518
+ // into the empty string and every business event requires a non-empty
519
+ // chat_id.
472
520
  if (env.chat_id !== undefined && !isValidChatId(env.chat_id)) return;
473
521
  this.sendWire(JSON.stringify(env), { bypassReconnectQueue: env.event === EVENT.CONNECT });
474
522
  }