@clawling/clawchat-plugin-openclaw 2026.8.4-2 → 2026.8.5-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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Synthetic reasoning-turn builder for `friend.request` notify.signal events.
3
+ *
4
+ * When the server fires a `friend.request` signal the runtime dispatches one
5
+ * deduped synthetic `message.send` envelope so the agent can reason about the
6
+ * pending request. The prompt text varies by the current "friend.add" permission
7
+ * policy state: a deny policy yields a decline/inform prompt; ask or allow
8
+ * yields a prompt that instructs the agent to review the request via the
9
+ * `clawchat_list_friend_requests` tool.
10
+ */
11
+ import { EVENT } from "./protocol-types.js";
12
+ /**
13
+ * Returns canned prompt text for a friend-request reasoning turn based on the
14
+ * current "friend.add" permission policy state.
15
+ *
16
+ * - `deny` → inform the agent it should decline; no accept instruction.
17
+ * - `ask` / `allow` → instruct the agent to review the pending request.
18
+ *
19
+ * The requester's user id is embedded so the agent does not go looking up the
20
+ * synthetic sender ("clawchat-friend-request" is not a real user).
21
+ */
22
+ export function friendRequestPromptFor(state, requesterUserId) {
23
+ const arrived = requesterUserId
24
+ ? `A new friend request has arrived from user ${requesterUserId}.`
25
+ : "A new friend request has arrived.";
26
+ if (state === "deny") {
27
+ return [
28
+ arrived,
29
+ "Your current friend-add policy is set to deny.",
30
+ "Do not add this contact.",
31
+ "You may inform the requester that you cannot add them at this time.",
32
+ ].join(" ");
33
+ }
34
+ return [
35
+ arrived,
36
+ "Please review the pending request by calling `clawchat_list_friend_requests`",
37
+ "and decide whether to accept it.",
38
+ ].join(" ");
39
+ }
40
+ /**
41
+ * Builds a synthetic `message.send` envelope that triggers one agent reasoning
42
+ * turn for a pending friend request. The envelope shape mirrors the
43
+ * activation-bootstrap envelope; it targets the owner's direct conversation so
44
+ * the agent has context about who it is reasoning for.
45
+ */
46
+ export function buildFriendRequestEnvelope(params) {
47
+ const { account, state, entityId, ownerConversationId } = params;
48
+ const text = friendRequestPromptFor(state, entityId);
49
+ const now = Date.now();
50
+ // Fall back to ownerUserId only when no activation conversation is recorded:
51
+ // the turn still runs (the agent can act via tools, e.g. accept the request),
52
+ // but its in-chat replies will not be deliverable until activation records
53
+ // the owner conversation.
54
+ return {
55
+ version: "2",
56
+ event: EVENT.MESSAGE_SEND,
57
+ trace_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
58
+ emitted_at: now,
59
+ chat_id: ownerConversationId ?? account.ownerUserId,
60
+ chat_type: "direct",
61
+ to: { id: account.userId, type: "direct" },
62
+ sender: {
63
+ id: "clawchat-friend-request",
64
+ type: "direct",
65
+ nick_name: "ClawChat",
66
+ },
67
+ payload: {
68
+ message_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
69
+ message_mode: "normal",
70
+ message: {
71
+ body: { fragments: [{ kind: "text", text }] },
72
+ context: { mentions: [], reply: null },
73
+ streaming: {
74
+ status: "static",
75
+ sequence: 0,
76
+ mutation_policy: "sealed",
77
+ started_at: null,
78
+ completed_at: null,
79
+ },
80
+ },
81
+ },
82
+ };
83
+ }
@@ -2803,6 +2803,37 @@ export async function startOpenclawClawlingGateway(params) {
2803
2803
  conversationId: claimedBootstrap.conversationId,
2804
2804
  }));
2805
2805
  };
2806
+ const markBootstrapSent = (reason) => {
2807
+ if (!bootstrap)
2808
+ return;
2809
+ const claimedBootstrap = bootstrap;
2810
+ const marked = recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
2811
+ platform: "openclaw",
2812
+ accountId,
2813
+ conversationId: claimedBootstrap.conversationId,
2814
+ }));
2815
+ if (marked !== true) {
2816
+ // The UPDATE is guarded on bootstrap_sent = 0 AND bootstrap_claimed_at
2817
+ // IS NOT NULL, so a stale re-claim makes this a no-op — and the row
2818
+ // then stays re-greetable on every later reconnect. Never swallow it.
2819
+ log?.error?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap not marked sent (${reason}); the greeting may repeat on reconnect`);
2820
+ }
2821
+ // Delivered — stop any pending retry.
2822
+ clearBootstrapRetry();
2823
+ };
2824
+ // Single decision point for every exit after the claim. The greeting is
2825
+ // emitted *inside* the dispatched turn, so once it has reached the user the
2826
+ // claim must never be rolled back: releasing it lets the next connection
2827
+ // claim again and greet a second time. Only a turn that delivered nothing
2828
+ // is safe to retry.
2829
+ const settleBootstrap = (reason) => {
2830
+ if (bootstrapGreetingDelivered) {
2831
+ markBootstrapSent(reason);
2832
+ return;
2833
+ }
2834
+ releaseBootstrap();
2835
+ armBootstrapRetry();
2836
+ };
2806
2837
  try {
2807
2838
  bootstrap = recordConnection("activation bootstrap claim", () => store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }));
2808
2839
  if (!bootstrap) {
@@ -2819,8 +2850,7 @@ export async function startOpenclawClawlingGateway(params) {
2819
2850
  bootstrapGreetingDelivered = false;
2820
2851
  const result = await handleInboundEnvelope(buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }));
2821
2852
  if (result !== "submitted") {
2822
- releaseBootstrap();
2823
- armBootstrapRetry();
2853
+ settleBootstrap("turn was not submitted");
2824
2854
  return;
2825
2855
  }
2826
2856
  // Only mark the bootstrap done once the greeting was actually delivered
@@ -2831,21 +2861,11 @@ export async function startOpenclawClawlingGateway(params) {
2831
2861
  // rather than silently consuming it forever or waiting for the next reconnect.
2832
2862
  if (!bootstrapGreetingDelivered) {
2833
2863
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but greeting was not delivered; leaving pending for retry`);
2834
- releaseBootstrap();
2835
- armBootstrapRetry();
2836
- return;
2837
2864
  }
2838
- recordConnection("activation bootstrap sent", () => store.markActivationBootstrapSent?.({
2839
- platform: "openclaw",
2840
- accountId,
2841
- conversationId: claimedBootstrap.conversationId,
2842
- }));
2843
- // Delivered — stop any pending retry.
2844
- clearBootstrapRetry();
2865
+ settleBootstrap("turn submitted");
2845
2866
  }
2846
2867
  catch (err) {
2847
- releaseBootstrap();
2848
- armBootstrapRetry();
2868
+ settleBootstrap("dispatch threw");
2849
2869
  log?.error?.(`[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
2850
2870
  }
2851
2871
  finally {
@@ -55,10 +55,18 @@ import { fileURLToPath } from "node:url";
55
55
  */
56
56
  export const OFFICIAL_SKILLS_BASE = "https://raw.githubusercontent.com/clawling/clawchat-plugin-install-cli";
57
57
  /**
58
- * Default git ref for the skills tree. Production callers SHOULD pin an
59
- * immutable `skills-vX.Y.Z` tag instead of tracking the moving `main`.
58
+ * Default git ref for the skills tree — an immutable `skills-vX.Y.Z` tag, not
59
+ * the moving `main`. Nothing supplies a target version today (the trigger
60
+ * signal is content-free), so this constant is the ref every fetch uses: on
61
+ * `main` a released plugin would follow whatever landed in the skills tree
62
+ * since, and convergence compares raw bytes, so an in-progress edit is enough
63
+ * to prompt the owner for an "update".
64
+ *
65
+ * Moving the pin is a deliberate release: publish the new `skills-vX.Y.Z` tag
66
+ * in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
67
+ * imports the same ref, so the `livewares` tree at that tag is pinned too.
60
68
  */
61
- export const DEFAULT_SKILLS_REF = "main";
69
+ export const DEFAULT_SKILLS_REF = "skills-v1.6.0";
62
70
  /** Refuse to treat an absurdly large response as a skill file (defence in depth). */
63
71
  export const MAX_SKILL_BYTES = 256 * 1024;
64
72
  /** This adapter's host target inside `skills/manifest.json`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.8.4-2",
3
+ "version": "2026.8.5-2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -29,10 +29,10 @@
29
29
  },
30
30
  "hermes": {
31
31
  "clawchat-core": {
32
- "version": "1.4.1",
32
+ "version": "1.7.0",
33
33
  "path": "hermes/clawchat-core/SKILL.md",
34
- "sha256": "33488a7a77a1bd950e2d791420370cac27cc9d8717042251d6df5f8b781dc904",
35
- "bytes": 11992
34
+ "sha256": "ea5118a99ddad7c9ce9c1437a4e33fbd76f45572998bb1722a70f4aa5e0e76aa",
35
+ "bytes": 17557
36
36
  },
37
37
  "clawchat-liveware": {
38
38
  "version": "1.1.0",
package/src/runtime.ts CHANGED
@@ -3333,6 +3333,40 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
3333
3333
  }),
3334
3334
  );
3335
3335
  };
3336
+ const markBootstrapSent = (reason: string) => {
3337
+ if (!bootstrap) return;
3338
+ const claimedBootstrap = bootstrap;
3339
+ const marked = recordConnection("activation bootstrap sent", () =>
3340
+ store.markActivationBootstrapSent?.({
3341
+ platform: "openclaw",
3342
+ accountId,
3343
+ conversationId: claimedBootstrap.conversationId,
3344
+ }),
3345
+ );
3346
+ if (marked !== true) {
3347
+ // The UPDATE is guarded on bootstrap_sent = 0 AND bootstrap_claimed_at
3348
+ // IS NOT NULL, so a stale re-claim makes this a no-op — and the row
3349
+ // then stays re-greetable on every later reconnect. Never swallow it.
3350
+ log?.error?.(
3351
+ `[${accountId}] clawchat-plugin-openclaw activation bootstrap not marked sent (${reason}); the greeting may repeat on reconnect`,
3352
+ );
3353
+ }
3354
+ // Delivered — stop any pending retry.
3355
+ clearBootstrapRetry();
3356
+ };
3357
+ // Single decision point for every exit after the claim. The greeting is
3358
+ // emitted *inside* the dispatched turn, so once it has reached the user the
3359
+ // claim must never be rolled back: releasing it lets the next connection
3360
+ // claim again and greet a second time. Only a turn that delivered nothing
3361
+ // is safe to retry.
3362
+ const settleBootstrap = (reason: string) => {
3363
+ if (bootstrapGreetingDelivered) {
3364
+ markBootstrapSent(reason);
3365
+ return;
3366
+ }
3367
+ releaseBootstrap();
3368
+ armBootstrapRetry();
3369
+ };
3336
3370
  try {
3337
3371
  bootstrap = recordConnection("activation bootstrap claim", () =>
3338
3372
  store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }),
@@ -3352,8 +3386,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
3352
3386
  buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
3353
3387
  );
3354
3388
  if (result !== "submitted") {
3355
- releaseBootstrap();
3356
- armBootstrapRetry();
3389
+ settleBootstrap("turn was not submitted");
3357
3390
  return;
3358
3391
  }
3359
3392
  // Only mark the bootstrap done once the greeting was actually delivered
@@ -3366,22 +3399,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
3366
3399
  log?.info?.(
3367
3400
  `[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but greeting was not delivered; leaving pending for retry`,
3368
3401
  );
3369
- releaseBootstrap();
3370
- armBootstrapRetry();
3371
- return;
3372
3402
  }
3373
- recordConnection("activation bootstrap sent", () =>
3374
- store.markActivationBootstrapSent?.({
3375
- platform: "openclaw",
3376
- accountId,
3377
- conversationId: claimedBootstrap.conversationId,
3378
- }),
3379
- );
3380
- // Delivered — stop any pending retry.
3381
- clearBootstrapRetry();
3403
+ settleBootstrap("turn submitted");
3382
3404
  } catch (err) {
3383
- releaseBootstrap();
3384
- armBootstrapRetry();
3405
+ settleBootstrap("dispatch threw");
3385
3406
  log?.error?.(
3386
3407
  `[${accountId}] clawchat-plugin-openclaw activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`,
3387
3408
  );
@@ -60,10 +60,18 @@ export const OFFICIAL_SKILLS_BASE =
60
60
  "https://raw.githubusercontent.com/clawling/clawchat-plugin-install-cli";
61
61
 
62
62
  /**
63
- * Default git ref for the skills tree. Production callers SHOULD pin an
64
- * immutable `skills-vX.Y.Z` tag instead of tracking the moving `main`.
63
+ * Default git ref for the skills tree — an immutable `skills-vX.Y.Z` tag, not
64
+ * the moving `main`. Nothing supplies a target version today (the trigger
65
+ * signal is content-free), so this constant is the ref every fetch uses: on
66
+ * `main` a released plugin would follow whatever landed in the skills tree
67
+ * since, and convergence compares raw bytes, so an in-progress edit is enough
68
+ * to prompt the owner for an "update".
69
+ *
70
+ * Moving the pin is a deliberate release: publish the new `skills-vX.Y.Z` tag
71
+ * in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
72
+ * imports the same ref, so the `livewares` tree at that tag is pinned too.
65
73
  */
66
- export const DEFAULT_SKILLS_REF = "main";
74
+ export const DEFAULT_SKILLS_REF = "skills-v1.6.0";
67
75
 
68
76
  /** Refuse to treat an absurdly large response as a skill file (defence in depth). */
69
77
  export const MAX_SKILL_BYTES = 256 * 1024;