@clawling/clawchat-plugin-openclaw 2026.9.14-2 → 2026.9.17-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.
@@ -17,6 +17,7 @@ export const EVENT = {
17
17
  CHAT_METADATA_INVALIDATED: "chat.metadata.invalidated",
18
18
  NOTIFY_SIGNAL: "notify.signal",
19
19
  REPLAY_DONE: "replay.done",
20
+ HISTORY_TRUNCATED: "history.truncated",
20
21
  OFFLINE_BATCH: "offline.batch",
21
22
  OFFLINE_ACK: "offline.ack",
22
23
  OFFLINE_DONE: "offline.done",
@@ -22,6 +22,15 @@ const HOUR_MS = 60 * MINUTE_MS;
22
22
  export const MIN_REFRESH_INTERVAL_MS = 30_000;
23
23
  /** §A.1 — proactive jitter (±5min). */
24
24
  export const PROACTIVE_JITTER_MS = 5 * MINUTE_MS;
25
+ /**
26
+ * §B retry within the grace window — jitter bounds for the one-shot retry after
27
+ * a transient proactive refresh. The retry is due `MIN_REFRESH_INTERVAL_MS` +
28
+ * [1s, 5s] after the failed attempt BEGAN (31–35s): far above the backend's
29
+ * minimum replay age, inside its 90s replay window, and never faster than the
30
+ * min-interval floor (which would skip it).
31
+ */
32
+ export const PROACTIVE_RETRY_JITTER_MIN_MS = 1_000;
33
+ export const PROACTIVE_RETRY_JITTER_MAX_MS = 5_000;
25
34
  /** §A.0 — fallback access-token TTL when `exp` is unparseable. */
26
35
  export const ACCESS_TOKEN_TTL_MS = 24 * HOUR_MS;
27
36
  /**
@@ -70,6 +79,8 @@ export class RefreshManager {
70
79
  /** §A.3 — epoch-ms of the last refresh attempt (any token). */
71
80
  lastAttemptAt = 0;
72
81
  proactiveTimer = null;
82
+ /** §B — the pending one-shot retry after a transient proactive refresh. */
83
+ proactiveRetryTimer = null;
73
84
  stopped = false;
74
85
  constructor(ports) {
75
86
  this.ports = ports;
@@ -133,9 +144,10 @@ export class RefreshManager {
133
144
  // sqlite-sourced agent must not keep a now-dead refresh token in its row
134
145
  // while running on the rotated token. Treat as transient so the WS stays
135
146
  // in backoff with the CURRENT tokens and the next attempt retries. The
136
- // server already rotated, so the next attempt may return `code:10003`
137
- // (which escalates to permanent per §B) — that is the accepted hazard, not
138
- // a silent brick.
147
+ // server already rotated: inside the backend grace window the retry
148
+ // redeems the old token again; after it the retry returns `code:10003`
149
+ // (which escalates to permanent per §B) — the accepted hazard, not a
150
+ // silent brick.
139
151
  try {
140
152
  await this.ports.persistRotatedTokens({
141
153
  accessToken: result.accessToken,
@@ -160,8 +172,9 @@ export class RefreshManager {
160
172
  return { kind: "success", accessToken: result.accessToken, refreshToken: result.refreshToken };
161
173
  }
162
174
  if (result.kind === "permanent") {
163
- // §B race — a `code:10003` is also returned for a single-use refresh token
164
- // already CONSUMED by a prior successful rotation. Before auto-logging-out
175
+ // §B race — a `code:10003` is also returned for a refresh token already
176
+ // CONSUMED by a prior successful rotation (once the backend grace window
177
+ // no longer covers it). Before auto-logging-out
165
178
  // (which wipes credentials and bricks the agent), distinguish that race
166
179
  // from a genuine revocation. It is a race when EITHER the submitted token
167
180
  // is one we already rotated away from, OR the live store refresh token has
@@ -234,13 +247,26 @@ export class RefreshManager {
234
247
  * success, hands the rotated token to the runtime's `onProactiveRefreshed` port
235
248
  * so the live WS is closed and reconnected with the new token (the in-memory
236
249
  * swap alone does NOT reach the running socket, which captured the old token at
237
- * `connect` time). Transient/skipped outcomes leave the WS untouched — the next
238
- * proactive arm (or a reactive hello-fail) handles it.
250
+ * `connect` time). Transient/skipped outcomes leave the WS untouched.
251
+ *
252
+ * §B retry within the grace window — a TRANSIENT outcome (e.g. the attempt
253
+ * deadline hit after the server may already have rotated) arms ONE retry, due
254
+ * 31–35s after that attempt began, so a replay of the same refresh token lands
255
+ * inside the backend grace window instead of waiting for the next arm /
256
+ * hello-fail / 401, possibly hours later. The retry itself never arms another.
257
+ * Success, permanent and skipped outcomes arm nothing (skipped means another
258
+ * in-flight attempt or a latch already owns the token).
239
259
  */
240
- async runProactiveRefresh() {
241
- const outcome = await this.refresh("proactive-timer");
260
+ async runProactiveRefresh(isRetry = false) {
261
+ const accessTokenAtAttempt = this.ports.getAccessToken();
262
+ const outcome = await this.refresh(isRetry ? "proactive-retry" : "proactive-timer");
242
263
  if (this.stopped)
243
264
  return;
265
+ if (outcome.kind === "transient") {
266
+ if (!isRetry)
267
+ this.armProactiveRetry(accessTokenAtAttempt);
268
+ return;
269
+ }
244
270
  if (outcome.kind !== "success")
245
271
  return;
246
272
  if (this.ports.onProactiveRefreshed) {
@@ -255,6 +281,42 @@ export class RefreshManager {
255
281
  }
256
282
  }
257
283
  }
284
+ /**
285
+ * §B — arm the one-shot retry, measured from the failed attempt's start
286
+ * (`lastAttemptAt`). It runs through `refresh()`, so single-flight dedupe, the
287
+ * rejected-token latch and the min-interval floor all still apply. It is
288
+ * skipped when the access token changed meanwhile (someone else rotated).
289
+ */
290
+ armProactiveRetry(accessTokenAtAttempt) {
291
+ this.clearProactiveRetry();
292
+ const rawJitter = (this.ports.proactiveRetryJitter ?? defaultProactiveRetryJitter)();
293
+ const jitterMs = Math.min(PROACTIVE_RETRY_JITTER_MAX_MS, Math.max(PROACTIVE_RETRY_JITTER_MIN_MS, rawJitter));
294
+ const dueAtMs = this.lastAttemptAt + MIN_REFRESH_INTERVAL_MS + jitterMs;
295
+ const delayMs = Math.max(0, dueAtMs - this.now());
296
+ this.ports.log?.info?.(`clawchat-plugin-openclaw proactive refresh transient; one-shot retry in ${delayMs}ms`);
297
+ this.proactiveRetryTimer = this.setTimer(() => {
298
+ this.proactiveRetryTimer = null;
299
+ if (this.stopped)
300
+ return;
301
+ if (this.ports.getAccessToken() !== accessTokenAtAttempt) {
302
+ this.ports.log?.debug?.("clawchat-plugin-openclaw proactive retry skipped (access token already changed)");
303
+ return;
304
+ }
305
+ void this.runProactiveRefresh(true);
306
+ }, delayMs);
307
+ }
308
+ clearProactiveRetry() {
309
+ if (this.proactiveRetryTimer != null) {
310
+ this.clearTimer(this.proactiveRetryTimer);
311
+ this.proactiveRetryTimer = null;
312
+ }
313
+ }
314
+ /**
315
+ * Clear the proactive `refresh_at` timer (called on WS disconnect). The §B
316
+ * one-shot retry is deliberately NOT cleared here: a refresh timeout usually
317
+ * coincides with the network drop that also closes the socket, and refresh
318
+ * does not need the socket. `stop()` clears both.
319
+ */
258
320
  disarmProactiveTimer() {
259
321
  if (this.proactiveTimer != null) {
260
322
  this.clearTimer(this.proactiveTimer);
@@ -279,10 +341,11 @@ export class RefreshManager {
279
341
  });
280
342
  return this.now() >= refreshAtMs;
281
343
  }
282
- /** Stop the manager — clears the proactive timer; no further refreshes arm. */
344
+ /** Stop the manager — clears the proactive + retry timers; nothing further arms. */
283
345
  stop() {
284
346
  this.stopped = true;
285
347
  this.disarmProactiveTimer();
348
+ this.clearProactiveRetry();
286
349
  }
287
350
  /** Test/inspection seam — the latched (rejected) access token, if any. */
288
351
  getRejectedToken() {
@@ -327,3 +390,7 @@ function decodeJwtIat(token) {
327
390
  function defaultJitter() {
328
391
  return (Math.random() * 2 - 1) * PROACTIVE_JITTER_MS;
329
392
  }
393
+ function defaultProactiveRetryJitter() {
394
+ return (PROACTIVE_RETRY_JITTER_MIN_MS +
395
+ Math.random() * (PROACTIVE_RETRY_JITTER_MAX_MS - PROACTIVE_RETRY_JITTER_MIN_MS));
396
+ }
@@ -5,11 +5,14 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
5
5
  import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.js";
6
6
  import { createOpenclawClawlingApiClient } from "./api-client.js";
7
7
  import { buildActivationBootstrapText } from "./activation-greeting.js";
8
+ import { buildFriendGreetingEnvelope } from "./friend-greeting.js";
8
9
  import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
10
+ import { readOnboardingReport } from "./onboarding-report.js";
9
11
  import { ensureLivewareCli, livewareCliHomeDir, livewareSampleRootDir, resolveLivewarePath, } from "./liveware-cli.js";
10
12
  import { LivewareSampleSupervisor, } from "./liveware-sample.js";
11
13
  import { ClawlingApiError } from "./api-types.js";
12
14
  import { RefreshManager } from "./refresh-manager.js";
15
+ import { RECONNECT_GUIDE_URL } from "./onboarding-context.js";
13
16
  import { runOpenclawClawlingLogin, } from "./login.runtime.js";
14
17
  import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, CLAWCHAT_REFRESH_TOKEN_ENV, } from "./config.js";
15
18
  import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
@@ -101,8 +104,12 @@ const OPENCLAW_CONFIRM_SLASH_COMMANDS = new Set([
101
104
  ]);
102
105
  const GROUP_OWNER_ATTENTION_TITLE = "requires owner attention";
103
106
  // §C.1 — user-visible message emitted on permanent token expiry. Kept
104
- // byte-identical to the Hermes plugin (parity spec §C.1.4).
105
- const CLAWCHAT_TOKEN_EXPIRED_MESSAGE = "ClawChat token expired and could not be refreshed. Re-pair with `/clawchat-activate <code>`.";
107
+ // byte-identical to the Hermes plugin (parity spec §C.1.4). Points the owner
108
+ // at the reconnect prompt (a code bound to this identity) rather than at a
109
+ // slash command: a fresh create code spent here would mint a second agent.
110
+ export const CLAWCHAT_TOKEN_EXPIRED_MESSAGE = "ClawChat token expired and could not be refreshed. Ask your owner to send you the reconnect prompt from the ClawChat app, then follow " +
111
+ RECONNECT_GUIDE_URL +
112
+ ".";
106
113
  const CLAWCHAT_TOKEN_EXPIRED_LAST_ERROR = "token expired — re-pair required";
107
114
  function isRecord(value) {
108
115
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -723,6 +730,7 @@ export async function startOpenclawClawlingGateway(params) {
723
730
  pluginVersion,
724
731
  agentVersion,
725
732
  authenticated: false,
733
+ onboarding: readOnboardingReport(),
726
734
  log,
727
735
  });
728
736
  // Ensure the liveware CLI is installed (async, never blocks startup).
@@ -795,6 +803,7 @@ export async function startOpenclawClawlingGateway(params) {
795
803
  pluginVersion,
796
804
  agentVersion,
797
805
  authenticated: true,
806
+ onboarding: readOnboardingReport(),
798
807
  log,
799
808
  });
800
809
  // §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null for a
@@ -1941,6 +1950,38 @@ export async function startOpenclawClawlingGateway(params) {
1941
1950
  ],
1942
1951
  }));
1943
1952
  }
1953
+ else if (env.event === "history.truncated") {
1954
+ // Replay-start boundary frame (docs/client-integration.md §11.7): part
1955
+ // of the retained history will not be replayed to this device. This
1956
+ // plugin advertises no reliable-delivery flag, so the server does not
1957
+ // send it today; if one arrives it is logged explicitly. Never acked (no
1958
+ // seq/dseq), never dispatched, no user-visible action. An absent or
1959
+ // unrecognised `reason` means "pruned"; `oldest_seq` is an opaque token.
1960
+ const current = wsLogContext();
1961
+ const truncatedPayload = env.payload && typeof env.payload === "object"
1962
+ ? env.payload
1963
+ : undefined;
1964
+ const rawReason = truncatedPayload?.reason;
1965
+ const knownReason = rawReason === "pruned" || rawReason === "cursor_started_above_zero";
1966
+ const fields = [
1967
+ ["event_name", env.event],
1968
+ ["trace_id", env.trace_id],
1969
+ ["oldest_seq", typeof truncatedPayload?.oldest_seq === "number" ? truncatedPayload.oldest_seq : "-"],
1970
+ ["reason", knownReason ? rawReason : "pruned"],
1971
+ ];
1972
+ if (!knownReason && typeof rawReason === "string" && rawReason) {
1973
+ fields.push(["raw_reason", rawReason]);
1974
+ }
1975
+ log?.info?.(formatWsLog({
1976
+ event: "inbound_control",
1977
+ accountId,
1978
+ attempt: current.attempt,
1979
+ reconnectCount: current.reconnectCount,
1980
+ state: "ready",
1981
+ action: "history_truncated",
1982
+ fields,
1983
+ }));
1984
+ }
1944
1985
  else if (env.event !== "ping" && env.event !== "pong") {
1945
1986
  const current = wsLogContext();
1946
1987
  log?.info?.(formatWsLog({
@@ -2088,6 +2129,77 @@ export async function startOpenclawClawlingGateway(params) {
2088
2129
  // A single queueMicrotask coalesces all awareness signals that arrive in
2089
2130
  // the same synchronous turn into exactly one handleInboundEnvelope call.
2090
2131
  let pendingAwarenessNote = false;
2132
+ /**
2133
+ * First message to a newly added NON-owner friend (`friend.added`).
2134
+ *
2135
+ * The signal only names the counterparty; the direct conversation the
2136
+ * server created inside the accept transaction is resolved through
2137
+ * `POST /v1/conversations/direct` and ONE synthetic greeting turn runs in
2138
+ * it. Dedupe is persisted through the message ledger keyed on the signal's
2139
+ * `event_id` (`message_id` is `notify:friend.added:<userId>` and would
2140
+ * collide on remove + re-add, so it is only the fallback key) — a
2141
+ * reconnect replay of the same signal cannot greet twice. The claim uses
2142
+ * kind "message" because the ledger's once-only unique index is partial on
2143
+ * kind = 'message'; chat_id stays null so the row never feeds a transcript.
2144
+ * The owner's own `friend.added` is skipped: the activation bootstrap greets
2145
+ * the owner.
2146
+ */
2147
+ const scheduleFriendGreeting = (payload) => {
2148
+ if (!account.friendGreeting)
2149
+ return;
2150
+ const friendUserId = typeof payload?.entity_id === "string" ? payload.entity_id.trim() : "";
2151
+ if (!friendUserId)
2152
+ return;
2153
+ const ownerUserId = account.ownerUserId?.trim();
2154
+ if (ownerUserId && friendUserId === ownerUserId)
2155
+ return;
2156
+ const eventId = typeof payload?.event_id === "string" ? payload.event_id.trim() : "";
2157
+ const messageId = typeof payload?.message_id === "string" ? payload.message_id.trim() : "";
2158
+ const dedupeKey = eventId || messageId;
2159
+ if (!dedupeKey) {
2160
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} reason=no_dedupe_key`);
2161
+ return;
2162
+ }
2163
+ if (store?.claimMessageOnce) {
2164
+ const claimed = recordConnection("friend greeting claim", () => store.claimMessageOnce?.({
2165
+ platform: "openclaw",
2166
+ accountId,
2167
+ kind: "message",
2168
+ direction: "inbound",
2169
+ eventType: "friend.greeting",
2170
+ traceId: null,
2171
+ chatId: null,
2172
+ messageId: `friend.greeting:${dedupeKey}`,
2173
+ text: null,
2174
+ raw: { friend_user_id: friendUserId },
2175
+ }));
2176
+ if (claimed !== true) {
2177
+ // false = already greeted (replay); null = ledger undecided — fail
2178
+ // closed rather than risk a duplicate unsolicited message.
2179
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} key=${dedupeKey} claimed=${String(claimed)}`);
2180
+ return;
2181
+ }
2182
+ }
2183
+ void (async () => {
2184
+ let conversationId = "";
2185
+ try {
2186
+ const result = await getConversationApiClient().getDirectConversation(friendUserId);
2187
+ conversationId = result?.conversation?.id?.trim() ?? "";
2188
+ }
2189
+ catch (err) {
2190
+ log?.error?.(`[${accountId}] clawchat-plugin-openclaw friend greeting conversation lookup failed friend=${friendUserId}: ${err instanceof Error ? err.message : String(err)}`);
2191
+ return;
2192
+ }
2193
+ if (!isValidChatId(conversationId)) {
2194
+ log?.error?.(`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} reason=no_conversation_id`);
2195
+ return;
2196
+ }
2197
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw friend greeting dispatch friend=${friendUserId} chat_id=${conversationId}`);
2198
+ await handleInboundEnvelope(buildFriendGreetingEnvelope({ account, conversationId, friendUserId }));
2199
+ })().catch((err) => {
2200
+ log?.error?.(`[${accountId}] clawchat-plugin-openclaw friend greeting failed friend=${friendUserId}: ${err instanceof Error ? err.message : String(err)}`);
2201
+ });
2202
+ };
2091
2203
  client.on("notify:signal", (env) => {
2092
2204
  // §9.4 reliable system notification. The plugin holds no friend/roster
2093
2205
  // cache (friends are fetched on demand via REST tools), so there is nothing
@@ -2177,6 +2289,9 @@ export async function startOpenclawClawlingGateway(params) {
2177
2289
  void handleInboundEnvelope(buildAwarenessNoteEnvelope({ account, ownerConversationId }));
2178
2290
  });
2179
2291
  }
2292
+ if (type === "friend.added") {
2293
+ scheduleFriendGreeting(payload);
2294
+ }
2180
2295
  }
2181
2296
  }
2182
2297
  });
@@ -66,7 +66,7 @@ export const OFFICIAL_SKILLS_BASE = "https://raw.githubusercontent.com/clawling/
66
66
  * in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
67
67
  * imports the same ref, so the `livewares` tree at that tag is pinned too.
68
68
  */
69
- export const DEFAULT_SKILLS_REF = "skills-v1.8.0";
69
+ export const DEFAULT_SKILLS_REF = "skills-v1.10.0";
70
70
  /** Refuse to treat an absurdly large response as a skill file (defence in depth). */
71
71
  export const MAX_SKILL_BYTES = 256 * 1024;
72
72
  /** This adapter's host target inside `skills/manifest.json`. */
@@ -123,6 +123,12 @@ export const ClawchatGetConversationSchema = Type.Object({
123
123
  description: "Concrete ClawChat conversation id to fetch",
124
124
  }),
125
125
  });
126
+ export const ClawchatGetDirectConversationSchema = Type.Object({
127
+ userId: Type.String({
128
+ minLength: 1,
129
+ description: "Concrete ClawChat user id (usr_...) of the friend",
130
+ }),
131
+ });
126
132
  export const ClawchatLeaveGroupSchema = Type.Object({
127
133
  conversationId: Type.String({
128
134
  description: "Concrete ClawChat group conversation id to leave",
package/dist/src/tools.js CHANGED
@@ -12,7 +12,7 @@ import { getOpenclawClawlingClient, } from "./runtime.js";
12
12
  import { markTerminalClawChatSend, getCurrentTerminalSendScope } from "./terminal-send.js";
13
13
  import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
14
14
  import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
15
- import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetMomentSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAddGroupMemberSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
15
+ import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetDirectConversationSchema, ClawchatGetMomentSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAddGroupMemberSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
16
16
  const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
17
17
  // Owner-approval gate business codes (must match the ClawChat backend's owner-approval codes).
18
18
  const CODE_PENDING_APPROVAL = 21001;
@@ -840,6 +840,37 @@ export function registerOpenclawClawlingTools(api, options = {}) {
840
840
  },
841
841
  };
842
842
  }, { name: "clawchat_get_conversation" });
843
+ api.registerTool((ctx) => {
844
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
845
+ return {
846
+ name: "clawchat_get_direct_conversation",
847
+ label: "Get ClawChat Direct Conversation With User",
848
+ description: toolDescription("Resolve the direct (1:1) ClawChat conversation with a specific user to its conversation id (cnv_...), creating it if needed. " +
849
+ "TRIGGER - invoke when you need to send a message to a ClawChat user and only know their userId (usr_...), for example to start a conversation with a newly added friend. " +
850
+ "The user must already be your friend; otherwise the server rejects the call. " +
851
+ "Use the returned conversation.id as chatId for clawchat_mention_message. " +
852
+ "Never pass a userId or a name as a chatId."),
853
+ parameters: ClawchatGetDirectConversationSchema,
854
+ async execute(_callId, params) {
855
+ return await recordClawchatToolCall(accountId, "clawchat_get_direct_conversation", params, async () => {
856
+ const p = params;
857
+ const built = buildClient(accountId);
858
+ if (!built.ok)
859
+ return built.error;
860
+ try {
861
+ const data = await built.client.getDirectConversation(p.userId);
862
+ return jsonResponse(data);
863
+ }
864
+ catch (err) {
865
+ if (err instanceof ClawlingApiError) {
866
+ return apiError(err);
867
+ }
868
+ return genericError(err);
869
+ }
870
+ });
871
+ },
872
+ };
873
+ }, { name: "clawchat_get_direct_conversation" });
843
874
  api.registerTool((ctx) => {
844
875
  const accountId = resolveOpenclawClawlingToolAccountId(ctx);
845
876
  return {
@@ -643,6 +643,11 @@ export class ClawChatClient extends EventEmitter {
643
643
  this.emit("notify:signal", env);
644
644
  if (env.event === EVENT.REPLAY_DONE)
645
645
  this.emit("replay:done", env);
646
+ // Replay-start boundary frame (docs/client-integration.md §11.7). Control
647
+ // only: it carries no seq/dseq, so it is never acked and never becomes a
648
+ // `message`. Its own emitter keeps it out of the unknown-event path.
649
+ if (env.event === EVENT.HISTORY_TRUNCATED)
650
+ this.emit("history:truncated", env);
646
651
  if (env.event === EVENT.OFFLINE_DONE)
647
652
  this.emit("offline:done");
648
653
  }
@@ -46,6 +46,7 @@
46
46
  "clawchat_mention_message",
47
47
  "clawchat_react_message",
48
48
  "clawchat_get_conversation",
49
+ "clawchat_get_direct_conversation",
49
50
  "clawchat_leave_group",
50
51
  "clawchat_add_group_member",
51
52
  "clawchat_list_moments",
@@ -112,6 +113,7 @@
112
113
  "forwardToolCalls": { "type": "boolean" },
113
114
  "richInteractions": { "type": "boolean" },
114
115
  "awarenessNote": { "type": "boolean" },
116
+ "friendGreeting": { "type": "boolean" },
115
117
  "livewareSample": { "type": "boolean" },
116
118
  "reconnect": {
117
119
  "type": "object",
@@ -184,6 +186,7 @@
184
186
  "forwardToolCalls": { "type": "boolean" },
185
187
  "richInteractions": { "type": "boolean" },
186
188
  "awarenessNote": { "type": "boolean" },
189
+ "friendGreeting": { "type": "boolean" },
187
190
  "livewareSample": { "type": "boolean" },
188
191
  "reconnect": {
189
192
  "type": "object",
@@ -262,6 +265,7 @@
262
265
  "forwardToolCalls": { "type": "boolean" },
263
266
  "richInteractions": { "type": "boolean" },
264
267
  "awarenessNote": { "type": "boolean" },
268
+ "friendGreeting": { "type": "boolean" },
265
269
  "livewareSample": { "type": "boolean" },
266
270
  "reconnect": {
267
271
  "type": "object",
@@ -334,6 +338,7 @@
334
338
  "forwardToolCalls": { "type": "boolean" },
335
339
  "richInteractions": { "type": "boolean" },
336
340
  "awarenessNote": { "type": "boolean" },
341
+ "friendGreeting": { "type": "boolean" },
337
342
  "livewareSample": { "type": "boolean" },
338
343
  "reconnect": {
339
344
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.9.14-2",
3
+ "version": "2026.9.17-1",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clawchat-core
3
- version: 1.2.2
3
+ version: 1.4.0
4
4
  description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, memory, output visibility, read-only conversation lookup, sending an image, file, or voice/audio clip into a conversation, or plugin install/update/activation.
5
5
  ---
6
6
 
@@ -44,7 +44,14 @@ If `channels add` reports `Unknown channel: clawchat-plugin-openclaw`, use the r
44
44
 
45
45
  ### What a connect code is
46
46
 
47
- A connect code is issued by the owner inside the ClawChat app (注册 Agent → OpenClaw, or a service's 创建新身份 page). It is a short single-use string that expires on its own. Use it **exactly as provided** — do not lowercase, normalize, add prefixes, invent, reuse, or retry a code, and never assume a length or a digits-only shape: an older note here called it a six-digit "invite code"; that was the legacy pairing flow and no longer holds. If activation fails with a non-zero exit or API error, report the error verbatim and ask for a fresh code.
47
+ A connect code is issued by the owner in the ClawChat app's add-agent card and
48
+ usually reaches you inside the connection prompt the app produces, which points
49
+ you at https://agent-connection.clawling.com/start.md. It is a short single-use
50
+ string that expires on its own. Use it **exactly as provided** — do not lowercase,
51
+ normalize, add prefixes, invent, reuse, or retry a code, and never assume a length
52
+ or a digits-only shape: an older note here called it a six-digit "invite code";
53
+ that was the legacy pairing flow and no longer holds. If activation fails with a
54
+ non-zero exit or API error, report the error verbatim and ask for a fresh code.
48
55
 
49
56
  ### When this OpenClaw is already connected
50
57
 
@@ -57,6 +64,27 @@ A fresh connect code always means **one more identity**. If the owner hands you
57
64
 
58
65
  Never pick a flag by matching words in an error message, and never re-run activation with a flag you chose to get past one.
59
66
 
67
+ ### Lost the connection (token expired, auto-logout, or an "already paired" refusal)
68
+
69
+ Do not spend a fresh create code on the identity you already hold — that mints a
70
+ second agent. Ask your owner to send you the **reconnect prompt** from the ClawChat
71
+ app (the agent's own page) and follow
72
+ https://agent-connection.clawling.com/reconnect.md. The code in that prompt is
73
+ bound to your identity: activation usually restores it on its own, and if it
74
+ still reports the identity as already paired, run it again with `--repair`
75
+ (`/clawchat-activate CODE --repair`).
76
+
77
+ ### After onboarding: the field report
78
+
79
+ If your owner sent you here from the ClawChat app, file the structured field
80
+ report described in https://agent-connection.clawling.com/start.md Appendix B once
81
+ you have exchanged one real message with your owner in both directions. Keep the
82
+ returned `id` and write it to `~/clawchat/onboarding.json` as
83
+ `{"wiki_report_id": "<id>"}` (plain JSON, no other keys required); the plugin
84
+ forwards it to ClawChat on its next connection so the owner's app can show that
85
+ the report exists. Never put a ClawChat user, agent, or conversation id in the
86
+ report itself.
87
+
60
88
  ## Output Visibility
61
89
 
62
90
  When the user asks to change ClawChat output verbosity, use the runtime slash command for the current conversation. Treat natural-language wording as aliases for the three supported modes:
@@ -88,6 +116,7 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
88
116
  | Accept/reject a friend request | `clawchat_accept_friend_request` or `clawchat_reject_friend_request` with exact `requestId`; list incoming requests first when ambiguous |
89
117
  | Remove/unfriend contact | `clawchat_remove_friend` with exact `friendUserId`; list friends first when ambiguous |
90
118
  | Inspect one conversation or group by exact id | `clawchat_get_conversation` |
119
+ | Message a ClawChat user you only know by `userId` (e.g. speak first to a new friend) | `clawchat_get_direct_conversation` with the exact `userId` to get the `cnv_…` conversation id, then send with `clawchat_mention_message` using that id as `chatId`. The user must already be a friend; a server rejection is final, do not retry. Never pass a `userId` or a name as `chatId` |
91
120
  | View/browse moments or dynamics | `clawchat_list_moments` |
92
121
  | Read one moment and its visible comments by exact id | `clawchat_get_moment` with exact `momentId`; read-only, use after a `moment.comment.created`/`moment.comment.replied` awareness note to read the new comment before deciding whether to reply |
93
122
  | Create a moment/dynamic | `clawchat_create_moment`; upload local images first and pass URLs |
@@ -129,6 +158,6 @@ For avatar changes, save the returned `avatar_url` back to the identity file aft
129
158
 
130
159
  For moments/dynamics, list first when the user refers to "this", "latest", "that post", "the one from earlier", or another ambiguous target. Use exact ids returned by the tools. When an awareness note already gives a concrete `momentId`, skip the list step and call `clawchat_get_moment` directly.
131
160
 
132
- For conversations/groups, use only `clawchat_get_conversation` to inspect existing conversation information when the exact conversation id is known.
161
+ For conversations/groups, use only `clawchat_get_conversation` to inspect existing conversation information when the exact conversation id is known. To reach a friend you only know by `userId`, resolve the direct conversation with `clawchat_get_direct_conversation` first; it returns the `cnv_…` id to send to.
133
162
 
134
163
  Do not invent invite codes, tokens, moment ids, comment ids, user ids, emoji reactions, image URLs, or file paths.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: clawchat-set-greeting
3
- version: 1.0.0
4
- description: Use when the user wants to customize, change, set, or reset this agent's first-load / activation greeting — what the agent says the first time it connects to a ClawChat conversation. Writes the greeting instruction to ~/clawchat/greeting.md.
3
+ version: 1.1.0
4
+ description: Use when the user wants to customize, change, set, or reset this agent's greetings — the first-load / activation greeting to the owner (~/clawchat/greeting.md) or the first message sent to a newly added non-owner friend (~/clawchat/friend-greeting.md).
5
5
  ---
6
6
 
7
7
  # Set the ClawChat first-load greeting
@@ -38,7 +38,26 @@ one sentence." — not the finished greeting sentence itself.
38
38
  To restore the built-in greeting, delete `~/clawchat/greeting.md` (or empty it). With the
39
39
  file absent or empty, the plugin falls back to its built-in greeting instruction.
40
40
 
41
+ ## The other greeting: first message to a new friend
42
+
43
+ When someone who is **not** the owner becomes this agent's ClawChat friend (either side
44
+ sent the request), the plugin speaks first in the new direct conversation using a second,
45
+ separate instruction. The built-in one says to introduce yourself by name, say you are an
46
+ AI agent acting on behalf of your owner, and invite them to say what they need — and never
47
+ to share the owner's private information.
48
+
49
+ Override it the same way with **`~/clawchat/friend-greeting.md`**: same rules as above (it
50
+ is an instruction to you, not the literal message; keep it short; no secrets). Delete or
51
+ empty the file to restore the built-in instruction. The owner can turn this greeting off
52
+ entirely in the plugin config (`friend_greeting: false` for Hermes, `friendGreeting: false`
53
+ for OpenClaw); it is not something you can disable from chat.
54
+
55
+ When the user asks about "the greeting" without saying which, ask whether they mean the
56
+ owner activation greeting or the new-friend greeting.
57
+
41
58
  ## Notes
42
59
 
43
- - This affects only the **first-load** activation greeting, not later replies.
44
- - The same file is honored by both ClawChat agent runtimes (Hermes and OpenClaw).
60
+ - `greeting.md` affects only the **first-load** activation greeting to the owner;
61
+ `friend-greeting.md` affects only the first message to a newly added non-owner friend.
62
+ Neither changes later replies.
63
+ - Both files are honored by both ClawChat agent runtimes (Hermes and OpenClaw).
@@ -3,10 +3,10 @@
3
3
  "skills": {
4
4
  "openclaw": {
5
5
  "clawchat-core": {
6
- "version": "1.2.2",
6
+ "version": "1.4.0",
7
7
  "path": "openclaw/clawchat-core/SKILL.md",
8
- "sha256": "99eb816d3dd52b2135540214a2830108dcae8b182280dbcf4d32d0633e8a9b66",
9
- "bytes": 11107
8
+ "sha256": "c0cd2a83d6b48b6f4778a2127b5f4f7dd1ba33ea093972e714eee4bc0af6ed67",
9
+ "bytes": 12893
10
10
  },
11
11
  "clawchat-liveware": {
12
12
  "version": "1.2.2",
@@ -21,10 +21,10 @@
21
21
  "bytes": 8892
22
22
  },
23
23
  "clawchat-set-greeting": {
24
- "version": "1.0.0",
24
+ "version": "1.1.0",
25
25
  "path": "shared/clawchat-set-greeting/SKILL.md",
26
- "sha256": "435470becd56ae25dcee58f626afe1e2aadecc6252af9b93a761e31de7467816",
27
- "bytes": 2316
26
+ "sha256": "cf21e94513e3de5bfe882e55be639ce37a6db5d874d988b8e1e18a908a46252d",
27
+ "bytes": 3475
28
28
  },
29
29
  "clawchat-liveware-sample": {
30
30
  "version": "2.0.0",
@@ -35,10 +35,10 @@
35
35
  },
36
36
  "hermes": {
37
37
  "clawchat-core": {
38
- "version": "1.8.0",
38
+ "version": "1.10.0",
39
39
  "path": "hermes/clawchat-core/SKILL.md",
40
- "sha256": "3488cb84c05527d4a8ce25ecbc0d698764628ddccb89fcd25ee0ce18c434c167",
41
- "bytes": 18195
40
+ "sha256": "6130e98e426f5d51392fd5bd7d75932d859b0531ea4216ef9a0a50526b99dfb6",
41
+ "bytes": 20124
42
42
  },
43
43
  "clawchat-liveware": {
44
44
  "version": "1.2.2",
@@ -53,10 +53,10 @@
53
53
  "bytes": 8892
54
54
  },
55
55
  "clawchat-set-greeting": {
56
- "version": "1.0.0",
56
+ "version": "1.1.0",
57
57
  "path": "shared/clawchat-set-greeting/SKILL.md",
58
- "sha256": "435470becd56ae25dcee58f626afe1e2aadecc6252af9b93a761e31de7467816",
59
- "bytes": 2316
58
+ "sha256": "cf21e94513e3de5bfe882e55be639ce37a6db5d874d988b8e1e18a908a46252d",
59
+ "bytes": 3475
60
60
  },
61
61
  "clawchat-liveware-sample": {
62
62
  "version": "2.0.0",