@clawling/clawchat-plugin-openclaw 2026.6.24-1 → 2026.6.30-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.
package/src/inbound.ts CHANGED
@@ -69,6 +69,18 @@ export interface DispatchInboundParams {
69
69
  runtime: PluginRuntime;
70
70
  account: ResolvedOpenclawClawlingAccount;
71
71
  ingest: (params: IngestTurnParams) => Promise<void>;
72
+ /**
73
+ * Pending-consent gate for conversational skill hot-update (see
74
+ * `src/skill-update.ts`). Called for direct messages from the owner BEFORE
75
+ * normal dispatch. Returns `true` when the reply was consumed as a consent
76
+ * decision (affirm/deny) and must NOT reach the LLM; `false` to let the
77
+ * message flow on normally (no pending update, or an ambiguous reply).
78
+ */
79
+ consumeOwnerConsent?: (args: {
80
+ senderId: string;
81
+ text: string;
82
+ chatId: string;
83
+ }) => Promise<boolean>;
72
84
  log?: { info?: (m: string) => void; error?: (m: string) => void };
73
85
  }
74
86
 
@@ -272,6 +284,35 @@ export async function dispatchOpenclawClawlingInbound(
272
284
  userId: account.userId,
273
285
  });
274
286
 
287
+ // Pending-consent gate for conversational skill hot-update. Only owner direct
288
+ // messages can answer a pending skill-update ask; the gate parses consent
289
+ // conservatively and, when it consumes the reply (affirm/deny), the message
290
+ // does NOT continue to the LLM. Ambiguous/unrelated replies return false and
291
+ // flow on normally (pending survives until timeout). See src/skill-update.ts.
292
+ if (
293
+ chatType === "direct" &&
294
+ params.consumeOwnerConsent &&
295
+ account.ownerUserId &&
296
+ sender.id === account.ownerUserId
297
+ ) {
298
+ let consumed = false;
299
+ try {
300
+ consumed = await params.consumeOwnerConsent({ senderId: sender.id, text: rawBody, chatId });
301
+ } catch (err) {
302
+ log?.error?.(
303
+ `[${account.accountId}] clawchat-plugin-openclaw skill-update consent gate error msg=${payload.message_id}: ${
304
+ err instanceof Error ? err.message : String(err)
305
+ }`,
306
+ );
307
+ }
308
+ if (consumed) {
309
+ log?.info?.(
310
+ `[${account.accountId}] clawchat-plugin-openclaw owner reply consumed as skill-update consent msg=${payload.message_id}`,
311
+ );
312
+ return;
313
+ }
314
+ }
315
+
275
316
  // NOTE: the static mention-mode early-drop was removed here (Task 7 fix).
276
317
  // Non-mention group messages now always flow through to ingestTurn, which
277
318
  // applies the dynamic gate (GroupSettingsCache.effective) and returns
@@ -2,6 +2,7 @@ import { createInterface, type Interface as ReadlineInterface } from "node:readl
2
2
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
3
3
  import { createOpenclawClawlingApiClient } from "./api-client.ts";
4
4
  import { ClawlingApiError, type AgentConnectResult } from "./api-types.ts";
5
+ import { resolvePluginVersion } from "./plugin-report.ts";
5
6
  import {
6
7
  CHANNEL_ID,
7
8
  mergeOpenclawClawchatRuntimePluginActivation,
@@ -197,6 +198,7 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
197
198
  // Pre-login we may not have a token yet. Send the current one (or empty)
198
199
  // — the server should accept an unauthenticated invite-code exchange.
199
200
  token: account.token || "",
201
+ pluginVersion: resolvePluginVersion(),
200
202
  });
201
203
 
202
204
  runtime.log("Verifying invite code …");
@@ -546,7 +546,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
546
546
 
547
547
  // §7.6 (binding): client-supplied message_id MUST be `msg-` + a 26-char
548
548
  // Crockford base32 ULID. Use the shared conformant minter — do not invent a
549
- // different scheme (planned msghub Phase-4 validation will reject it).
549
+ // different scheme (planned backend validation will reject it).
550
550
  const mintStaticMessageId = () => mintMessageId();
551
551
 
552
552
  const emitTyping = (isTyping: boolean) => {
package/src/runtime.ts CHANGED
@@ -35,6 +35,14 @@ import {
35
35
  } from "./config.ts";
36
36
  import type { ClawlingChatClient } from "./ws-client.ts";
37
37
  import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
38
+ import {
39
+ PendingConsentStore,
40
+ handleOwnerConsentReply,
41
+ readEffectiveSkillVersions,
42
+ resolveBundledSkillsDir,
43
+ resolveManagedSkillsDir,
44
+ runSkillUpdateCheck,
45
+ } from "./skill-update.ts";
38
46
  import { fetchInboundMedia } from "./media-runtime.ts";
39
47
  import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.ts";
40
48
  import { runWithTerminalClawChatSendScope } from "./terminal-send.ts";
@@ -419,12 +427,17 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
419
427
  }
420
428
 
421
429
  function buildActivationBootstrapText(): string {
430
+ // Worded to make the greeting the single required action. Earlier wording
431
+ // ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
432
+ // it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
433
+ // no greeting reached the user. Lead with the chat reply, forbid file/tool
434
+ // detours, and keep the profile update strictly optional and secondary.
422
435
  return [
423
- "ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
424
- "Please do both:",
425
- "1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
426
- "2. If you have local profile information for yourself, such as display name, bio, or avatar, update the connected ClawChat account profile using the available ClawChat tools. Use `clawchat_update_account_profile` for display name/bio/avatar URL, and use `clawchat_upload_avatar_image` first if the avatar is only available as a local image path. If you do not have local profile information, skip profile updates and only greet the user.",
427
- "Do not ask the user for profile information just for this bootstrap.",
436
+ "You are now connected to a ClawChat direct conversation with your user.",
437
+ "Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
438
+ "Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
439
+ "Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
440
+ "Do not ask the user for profile information.",
428
441
  ].join("\n");
429
442
  }
430
443
 
@@ -1808,6 +1821,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1808
1821
  );
1809
1822
  };
1810
1823
  let dispatchActivationBootstrap: () => Promise<void> = async () => {};
1824
+ // Set by dispatchTurnToAgent when it dispatches the synthetic
1825
+ // `clawchat-bootstrap` turn: true only if the agent actually produced a final
1826
+ // outbound message (the greeting). The bootstrap dispatcher reads it to decide
1827
+ // whether to mark the bootstrap sent or leave it pending for a later retry.
1828
+ let bootstrapProducedFinal = false;
1811
1829
 
1812
1830
  client.on("state", ({ from, to }) => {
1813
1831
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw state ${from} -> ${to}`);
@@ -2038,6 +2056,83 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2038
2056
  void handleMetadataInvalidation(env);
2039
2057
  });
2040
2058
 
2059
+ // --- Conversational skill hot-update (adapter-driven; see src/skill-update.ts) ---
2060
+ // One pending-consent record per runtime.
2061
+ // WRITE target = the OpenClaw-managed skills dir (`<stateDir>/skills`): it is
2062
+ // chokidar-watched (host src/skills/runtime/refresh.ts:107) so a write bumps
2063
+ // the snapshot + rebuilds the prompt next turn, and it loads at HIGHER
2064
+ // precedence than plugin-bundled skills (managed=3 > bundled=2, host
2065
+ // src/skills/loading/workspace.ts:1220-1239), overriding the bundled copy.
2066
+ // The bundled `./skills` dir is READ-ONLY here: a first-boot fallback for the
2067
+ // "current installed version" only — never written (it lives in node_modules
2068
+ // → may be read-only and is clobbered on `openclaw plugins update`).
2069
+ const skillUpdateConsent = new PendingConsentStore();
2070
+ const managedSkillsDir = resolveManagedSkillsDir();
2071
+ let bundledSkillsDir: string | null = null;
2072
+ try {
2073
+ bundledSkillsDir = resolveBundledSkillsDir();
2074
+ } catch (err) {
2075
+ log?.info?.(
2076
+ `[${accountId}] clawchat-plugin-openclaw skill-update: bundled skills dir not resolvable (using managed-only current): ${
2077
+ err instanceof Error ? err.message : String(err)
2078
+ }`,
2079
+ );
2080
+ }
2081
+ const sendOwnerSkillMessage = async (text: string): Promise<void> => {
2082
+ const ownerUserId = account.ownerUserId?.trim();
2083
+ if (!ownerUserId) return;
2084
+ await sendOpenclawClawlingText({
2085
+ client,
2086
+ account,
2087
+ to: { chatId: ownerUserId, chatType: "direct" },
2088
+ text,
2089
+ log,
2090
+ });
2091
+ };
2092
+ const triggerSkillUpdateCheck = async (): Promise<void> => {
2093
+ const ownerUserId = account.ownerUserId?.trim();
2094
+ if (!ownerUserId) {
2095
+ log?.info?.(
2096
+ `[${accountId}] clawchat-plugin-openclaw skill-update check skipped: no owner user id`,
2097
+ );
2098
+ return;
2099
+ }
2100
+ // Effective current = managed copy if present, else bundled (host precedence).
2101
+ const localVersions = await readEffectiveSkillVersions(managedSkillsDir, bundledSkillsDir);
2102
+ await runSkillUpdateCheck({
2103
+ ownerUserId,
2104
+ localVersions,
2105
+ fetchFn: globalThis.fetch,
2106
+ store: skillUpdateConsent,
2107
+ sendOwnerMessage: sendOwnerSkillMessage,
2108
+ log: {
2109
+ info: (m) => log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
2110
+ error: (m) => log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
2111
+ },
2112
+ });
2113
+ };
2114
+ const consumeOwnerSkillConsent = async (args: {
2115
+ senderId: string;
2116
+ text: string;
2117
+ chatId: string;
2118
+ }): Promise<boolean> => {
2119
+ const ownerUserId = account.ownerUserId?.trim();
2120
+ if (!ownerUserId) return false;
2121
+ return handleOwnerConsentReply({
2122
+ ownerUserId,
2123
+ senderId: args.senderId,
2124
+ text: args.text,
2125
+ store: skillUpdateConsent,
2126
+ fetchFn: globalThis.fetch,
2127
+ skillsDir: managedSkillsDir,
2128
+ sendOwnerMessage: sendOwnerSkillMessage,
2129
+ log: {
2130
+ info: (m) => log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
2131
+ error: (m) => log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${m}`),
2132
+ },
2133
+ });
2134
+ };
2135
+
2041
2136
  client.on("notify:signal", (env: Envelope) => {
2042
2137
  // §9.4 reliable system notification. The plugin holds no friend/roster
2043
2138
  // cache (friends are fetched on demand via REST tools), so there is nothing
@@ -2054,6 +2149,20 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2054
2149
  `[${accountId}] clawchat-plugin-openclaw agent.config.changed signal received; re-pulling group settings`,
2055
2150
  );
2056
2151
  pullGroupSettings();
2152
+ } else if (type === "clawchat.skill.update.check") {
2153
+ // Content-free trigger: check the official skill source in code and, if
2154
+ // there is an update, ask the owner in chat for consent. The atomic
2155
+ // overwrite happens later when the owner affirms (consent gate).
2156
+ log?.info?.(
2157
+ `[${accountId}] clawchat-plugin-openclaw clawchat.skill.update.check signal received; checking official skill source`,
2158
+ );
2159
+ void triggerSkillUpdateCheck().catch((err) => {
2160
+ log?.error?.(
2161
+ `[${accountId}] clawchat-plugin-openclaw skill-update check failed: ${
2162
+ err instanceof Error ? err.message : String(err)
2163
+ }`,
2164
+ );
2165
+ });
2057
2166
  }
2058
2167
  }
2059
2168
  });
@@ -2480,6 +2589,12 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2480
2589
  const queuedFinal = Boolean(
2481
2590
  (dispatchResult as { queuedFinal?: boolean } | undefined)?.queuedFinal,
2482
2591
  );
2592
+ if (turn.senderId === "clawchat-bootstrap") {
2593
+ // Did the agent actually emit a greeting? A final outbound message
2594
+ // means yes; block/tool-only (or nothing) means the greeting never
2595
+ // reached the user, so the bootstrap dispatcher will keep it pending.
2596
+ bootstrapProducedFinal = queuedFinal || (counts.final ?? 0) > 0;
2597
+ }
2483
2598
  log?.info?.(
2484
2599
  `[${accountId}] clawchat-plugin-openclaw dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`,
2485
2600
  );
@@ -2744,6 +2859,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2744
2859
  runtime,
2745
2860
  account,
2746
2861
  log,
2862
+ consumeOwnerConsent: consumeOwnerSkillConsent,
2747
2863
  ingest: async (turn) => {
2748
2864
  ingestResult = await ingestTurn(turn);
2749
2865
  },
@@ -2777,6 +2893,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2777
2893
  );
2778
2894
  if (!bootstrap) return;
2779
2895
  const claimedBootstrap = bootstrap;
2896
+ bootstrapProducedFinal = false;
2780
2897
  const result = await handleInboundEnvelope(
2781
2898
  buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
2782
2899
  );
@@ -2784,6 +2901,18 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2784
2901
  releaseBootstrap();
2785
2902
  return;
2786
2903
  }
2904
+ // Only mark the bootstrap done once the agent has actually sent a greeting
2905
+ // (a final outbound message). If the turn was handed off but produced no
2906
+ // final reply — e.g. the agent went off to write a file/note instead of
2907
+ // replying — leave the bootstrap pending (release the claim) so the next
2908
+ // ready re-dispatches it, rather than silently consuming it forever.
2909
+ if (!bootstrapProducedFinal) {
2910
+ log?.info?.(
2911
+ `[${accountId}] clawchat-plugin-openclaw activation bootstrap dispatched but produced no final reply; leaving pending for retry`,
2912
+ );
2913
+ releaseBootstrap();
2914
+ return;
2915
+ }
2787
2916
  recordConnection("activation bootstrap sent", () =>
2788
2917
  store.markActivationBootstrapSent?.({
2789
2918
  platform: "openclaw",