@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.
- package/dist/src/api-client.js +84 -9
- package/dist/src/config.js +3 -0
- package/dist/src/connect-check.js +74 -0
- package/dist/src/friend-greeting.js +80 -0
- package/dist/src/login.runtime.js +84 -39
- package/dist/src/onboarding-context.js +70 -0
- package/dist/src/onboarding-report.js +50 -0
- package/dist/src/plugin-report.js +1 -0
- package/dist/src/protocol-types.js +1 -0
- package/dist/src/refresh-manager.js +77 -10
- package/dist/src/runtime.js +117 -2
- package/dist/src/skill-update.js +1 -1
- package/dist/src/tools-schema.js +6 -0
- package/dist/src/tools.js +32 -1
- package/dist/src/ws-client.js +5 -0
- package/openclaw.plugin.json +5 -0
- package/package.json +1 -1
- package/skills/clawchat-core/SKILL.md +32 -3
- package/skills/clawchat-set-greeting/SKILL.md +23 -4
- package/skills/manifest.json +12 -12
- package/src/api-client.ts +116 -23
- package/src/api-types.ts +18 -0
- package/src/config.ts +7 -0
- package/src/connect-check.ts +98 -0
- package/src/friend-greeting.ts +91 -0
- package/src/login.runtime.ts +105 -44
- package/src/onboarding-context.ts +77 -0
- package/src/onboarding-report.ts +60 -0
- package/src/plugin-report.ts +4 -0
- package/src/protocol-types.ts +1 -0
- package/src/refresh-manager.ts +89 -10
- package/src/runtime.ts +135 -3
- package/src/skill-update.ts +1 -1
- package/src/tools-schema.ts +8 -0
- package/src/tools.ts +37 -0
- package/src/ws-client.ts +4 -0
|
@@ -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
|
|
137
|
-
//
|
|
138
|
-
//
|
|
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
|
|
164
|
-
//
|
|
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
|
|
238
|
-
*
|
|
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
|
|
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
|
|
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
|
+
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|
package/dist/src/skill-update.js
CHANGED
|
@@ -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.
|
|
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`. */
|
package/dist/src/tools-schema.js
CHANGED
|
@@ -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 {
|
package/dist/src/ws-client.js
CHANGED
|
@@ -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
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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: clawchat-core
|
|
3
|
-
version: 1.
|
|
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
|
|
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.
|
|
4
|
-
description: Use when the user wants to customize, change, set, or reset this agent's first-load / activation greeting
|
|
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
|
-
-
|
|
44
|
-
-
|
|
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).
|
package/skills/manifest.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"skills": {
|
|
4
4
|
"openclaw": {
|
|
5
5
|
"clawchat-core": {
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.4.0",
|
|
7
7
|
"path": "openclaw/clawchat-core/SKILL.md",
|
|
8
|
-
"sha256": "
|
|
9
|
-
"bytes":
|
|
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.
|
|
24
|
+
"version": "1.1.0",
|
|
25
25
|
"path": "shared/clawchat-set-greeting/SKILL.md",
|
|
26
|
-
"sha256": "
|
|
27
|
-
"bytes":
|
|
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.
|
|
38
|
+
"version": "1.10.0",
|
|
39
39
|
"path": "hermes/clawchat-core/SKILL.md",
|
|
40
|
-
"sha256": "
|
|
41
|
-
"bytes":
|
|
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.
|
|
56
|
+
"version": "1.1.0",
|
|
57
57
|
"path": "shared/clawchat-set-greeting/SKILL.md",
|
|
58
|
-
"sha256": "
|
|
59
|
-
"bytes":
|
|
58
|
+
"sha256": "cf21e94513e3de5bfe882e55be639ce37a6db5d874d988b8e1e18a908a46252d",
|
|
59
|
+
"bytes": 3475
|
|
60
60
|
},
|
|
61
61
|
"clawchat-liveware-sample": {
|
|
62
62
|
"version": "2.0.0",
|