@clawling/clawchat-plugin-openclaw 2026.9.14-2 → 2026.9.16-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 +52 -7
- package/dist/src/config.js +3 -0
- package/dist/src/friend-greeting.js +80 -0
- package/dist/src/protocol-types.js +1 -0
- package/dist/src/refresh-manager.js +77 -10
- package/dist/src/runtime.js +107 -0
- 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 +3 -2
- package/skills/clawchat-set-greeting/SKILL.md +23 -4
- package/skills/manifest.json +12 -12
- package/src/api-client.ts +65 -8
- package/src/config.ts +7 -0
- package/src/friend-greeting.ts +91 -0
- package/src/protocol-types.ts +1 -0
- package/src/refresh-manager.ts +89 -10
- package/src/runtime.ts +124 -0
- 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
package/src/runtime.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
|
17
17
|
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.ts";
|
|
18
18
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
19
19
|
import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
20
|
+
import { buildFriendGreetingEnvelope } from "./friend-greeting.ts";
|
|
20
21
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
import {
|
|
@@ -2368,6 +2369,39 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2368
2369
|
],
|
|
2369
2370
|
}),
|
|
2370
2371
|
);
|
|
2372
|
+
} else if (env.event === "history.truncated") {
|
|
2373
|
+
// Replay-start boundary frame (docs/client-integration.md §11.7): part
|
|
2374
|
+
// of the retained history will not be replayed to this device. This
|
|
2375
|
+
// plugin advertises no reliable-delivery flag, so the server does not
|
|
2376
|
+
// send it today; if one arrives it is logged explicitly. Never acked (no
|
|
2377
|
+
// seq/dseq), never dispatched, no user-visible action. An absent or
|
|
2378
|
+
// unrecognised `reason` means "pruned"; `oldest_seq` is an opaque token.
|
|
2379
|
+
const current = wsLogContext();
|
|
2380
|
+
const truncatedPayload = env.payload && typeof env.payload === "object"
|
|
2381
|
+
? (env.payload as { oldest_seq?: unknown; reason?: unknown })
|
|
2382
|
+
: undefined;
|
|
2383
|
+
const rawReason = truncatedPayload?.reason;
|
|
2384
|
+
const knownReason = rawReason === "pruned" || rawReason === "cursor_started_above_zero";
|
|
2385
|
+
const fields: Array<[string, string | number]> = [
|
|
2386
|
+
["event_name", env.event],
|
|
2387
|
+
["trace_id", env.trace_id],
|
|
2388
|
+
["oldest_seq", typeof truncatedPayload?.oldest_seq === "number" ? truncatedPayload.oldest_seq : "-"],
|
|
2389
|
+
["reason", knownReason ? (rawReason as string) : "pruned"],
|
|
2390
|
+
];
|
|
2391
|
+
if (!knownReason && typeof rawReason === "string" && rawReason) {
|
|
2392
|
+
fields.push(["raw_reason", rawReason]);
|
|
2393
|
+
}
|
|
2394
|
+
log?.info?.(
|
|
2395
|
+
formatWsLog({
|
|
2396
|
+
event: "inbound_control",
|
|
2397
|
+
accountId,
|
|
2398
|
+
attempt: current.attempt,
|
|
2399
|
+
reconnectCount: current.reconnectCount,
|
|
2400
|
+
state: "ready",
|
|
2401
|
+
action: "history_truncated",
|
|
2402
|
+
fields,
|
|
2403
|
+
}),
|
|
2404
|
+
);
|
|
2371
2405
|
} else if (env.event !== "ping" && env.event !== "pong") {
|
|
2372
2406
|
const current = wsLogContext();
|
|
2373
2407
|
log?.info?.(
|
|
@@ -2527,6 +2561,93 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2527
2561
|
// A single queueMicrotask coalesces all awareness signals that arrive in
|
|
2528
2562
|
// the same synchronous turn into exactly one handleInboundEnvelope call.
|
|
2529
2563
|
let pendingAwarenessNote = false;
|
|
2564
|
+
/**
|
|
2565
|
+
* First message to a newly added NON-owner friend (`friend.added`).
|
|
2566
|
+
*
|
|
2567
|
+
* The signal only names the counterparty; the direct conversation the
|
|
2568
|
+
* server created inside the accept transaction is resolved through
|
|
2569
|
+
* `POST /v1/conversations/direct` and ONE synthetic greeting turn runs in
|
|
2570
|
+
* it. Dedupe is persisted through the message ledger keyed on the signal's
|
|
2571
|
+
* `event_id` (`message_id` is `notify:friend.added:<userId>` and would
|
|
2572
|
+
* collide on remove + re-add, so it is only the fallback key) — a
|
|
2573
|
+
* reconnect replay of the same signal cannot greet twice. The claim uses
|
|
2574
|
+
* kind "message" because the ledger's once-only unique index is partial on
|
|
2575
|
+
* kind = 'message'; chat_id stays null so the row never feeds a transcript.
|
|
2576
|
+
* The owner's own `friend.added` is skipped: the activation bootstrap greets
|
|
2577
|
+
* the owner.
|
|
2578
|
+
*/
|
|
2579
|
+
const scheduleFriendGreeting = (payload: Record<string, unknown> | undefined): void => {
|
|
2580
|
+
if (!account.friendGreeting) return;
|
|
2581
|
+
const friendUserId = typeof payload?.entity_id === "string" ? payload.entity_id.trim() : "";
|
|
2582
|
+
if (!friendUserId) return;
|
|
2583
|
+
const ownerUserId = account.ownerUserId?.trim();
|
|
2584
|
+
if (ownerUserId && friendUserId === ownerUserId) return;
|
|
2585
|
+
const eventId = typeof payload?.event_id === "string" ? payload.event_id.trim() : "";
|
|
2586
|
+
const messageId = typeof payload?.message_id === "string" ? payload.message_id.trim() : "";
|
|
2587
|
+
const dedupeKey = eventId || messageId;
|
|
2588
|
+
if (!dedupeKey) {
|
|
2589
|
+
log?.info?.(
|
|
2590
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} reason=no_dedupe_key`,
|
|
2591
|
+
);
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
if (store?.claimMessageOnce) {
|
|
2595
|
+
const claimed = recordConnection("friend greeting claim", () =>
|
|
2596
|
+
store.claimMessageOnce?.({
|
|
2597
|
+
platform: "openclaw",
|
|
2598
|
+
accountId,
|
|
2599
|
+
kind: "message",
|
|
2600
|
+
direction: "inbound",
|
|
2601
|
+
eventType: "friend.greeting",
|
|
2602
|
+
traceId: null,
|
|
2603
|
+
chatId: null,
|
|
2604
|
+
messageId: `friend.greeting:${dedupeKey}`,
|
|
2605
|
+
text: null,
|
|
2606
|
+
raw: { friend_user_id: friendUserId },
|
|
2607
|
+
}),
|
|
2608
|
+
);
|
|
2609
|
+
if (claimed !== true) {
|
|
2610
|
+
// false = already greeted (replay); null = ledger undecided — fail
|
|
2611
|
+
// closed rather than risk a duplicate unsolicited message.
|
|
2612
|
+
log?.info?.(
|
|
2613
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} key=${dedupeKey} claimed=${String(claimed)}`,
|
|
2614
|
+
);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
void (async () => {
|
|
2619
|
+
let conversationId = "";
|
|
2620
|
+
try {
|
|
2621
|
+
const result = await getConversationApiClient().getDirectConversation(friendUserId);
|
|
2622
|
+
conversationId = result?.conversation?.id?.trim() ?? "";
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
log?.error?.(
|
|
2625
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting conversation lookup failed friend=${friendUserId}: ${
|
|
2626
|
+
err instanceof Error ? err.message : String(err)
|
|
2627
|
+
}`,
|
|
2628
|
+
);
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (!isValidChatId(conversationId)) {
|
|
2632
|
+
log?.error?.(
|
|
2633
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting skipped friend=${friendUserId} reason=no_conversation_id`,
|
|
2634
|
+
);
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
log?.info?.(
|
|
2638
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting dispatch friend=${friendUserId} chat_id=${conversationId}`,
|
|
2639
|
+
);
|
|
2640
|
+
await handleInboundEnvelope(
|
|
2641
|
+
buildFriendGreetingEnvelope({ account, conversationId, friendUserId }),
|
|
2642
|
+
);
|
|
2643
|
+
})().catch((err) => {
|
|
2644
|
+
log?.error?.(
|
|
2645
|
+
`[${accountId}] clawchat-plugin-openclaw friend greeting failed friend=${friendUserId}: ${
|
|
2646
|
+
err instanceof Error ? err.message : String(err)
|
|
2647
|
+
}`,
|
|
2648
|
+
);
|
|
2649
|
+
});
|
|
2650
|
+
};
|
|
2530
2651
|
|
|
2531
2652
|
client.on("notify:signal", (env: Envelope) => {
|
|
2532
2653
|
// §9.4 reliable system notification. The plugin holds no friend/roster
|
|
@@ -2641,6 +2762,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2641
2762
|
);
|
|
2642
2763
|
});
|
|
2643
2764
|
}
|
|
2765
|
+
if (type === "friend.added") {
|
|
2766
|
+
scheduleFriendGreeting(payload as Record<string, unknown> | undefined);
|
|
2767
|
+
}
|
|
2644
2768
|
}
|
|
2645
2769
|
}
|
|
2646
2770
|
});
|
package/src/skill-update.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const OFFICIAL_SKILLS_BASE =
|
|
|
71
71
|
* in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
|
|
72
72
|
* imports the same ref, so the `livewares` tree at that tag is pinned too.
|
|
73
73
|
*/
|
|
74
|
-
export const DEFAULT_SKILLS_REF = "skills-v1.
|
|
74
|
+
export const DEFAULT_SKILLS_REF = "skills-v1.9.0";
|
|
75
75
|
|
|
76
76
|
/** Refuse to treat an absurdly large response as a skill file (defence in depth). */
|
|
77
77
|
export const MAX_SKILL_BYTES = 256 * 1024;
|
package/src/tools-schema.ts
CHANGED
|
@@ -201,6 +201,14 @@ export const ClawchatGetConversationSchema = Type.Object({
|
|
|
201
201
|
});
|
|
202
202
|
export type ClawchatGetConversationParams = Static<typeof ClawchatGetConversationSchema>;
|
|
203
203
|
|
|
204
|
+
export const ClawchatGetDirectConversationSchema = Type.Object({
|
|
205
|
+
userId: Type.String({
|
|
206
|
+
minLength: 1,
|
|
207
|
+
description: "Concrete ClawChat user id (usr_...) of the friend",
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
export type ClawchatGetDirectConversationParams = Static<typeof ClawchatGetDirectConversationSchema>;
|
|
211
|
+
|
|
204
212
|
export const ClawchatLeaveGroupSchema = Type.Object({
|
|
205
213
|
conversationId: Type.String({
|
|
206
214
|
description: "Concrete ClawChat group conversation id to leave",
|
package/src/tools.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import {
|
|
45
45
|
ClawchatGetAccountProfileSchema,
|
|
46
46
|
ClawchatGetConversationSchema,
|
|
47
|
+
ClawchatGetDirectConversationSchema,
|
|
47
48
|
ClawchatGetMomentSchema,
|
|
48
49
|
ClawchatGetUserProfileSchema,
|
|
49
50
|
ClawchatLeaveGroupSchema,
|
|
@@ -88,6 +89,7 @@ import {
|
|
|
88
89
|
type ClawchatDeleteMomentParams,
|
|
89
90
|
type ClawchatAcceptFriendRequestParams,
|
|
90
91
|
type ClawchatGetConversationParams,
|
|
92
|
+
type ClawchatGetDirectConversationParams,
|
|
91
93
|
type ClawchatGetMomentParams,
|
|
92
94
|
type ClawchatGetUserProfileParams,
|
|
93
95
|
type ClawchatLeaveGroupParams,
|
|
@@ -1090,6 +1092,41 @@ export function registerOpenclawClawlingTools(
|
|
|
1090
1092
|
{ name: "clawchat_get_conversation" },
|
|
1091
1093
|
);
|
|
1092
1094
|
|
|
1095
|
+
api.registerTool(
|
|
1096
|
+
(ctx) => {
|
|
1097
|
+
const accountId = resolveOpenclawClawlingToolAccountId(ctx);
|
|
1098
|
+
return {
|
|
1099
|
+
name: "clawchat_get_direct_conversation",
|
|
1100
|
+
label: "Get ClawChat Direct Conversation With User",
|
|
1101
|
+
description: toolDescription(
|
|
1102
|
+
"Resolve the direct (1:1) ClawChat conversation with a specific user to its conversation id (cnv_...), creating it if needed. " +
|
|
1103
|
+
"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. " +
|
|
1104
|
+
"The user must already be your friend; otherwise the server rejects the call. " +
|
|
1105
|
+
"Use the returned conversation.id as chatId for clawchat_mention_message. " +
|
|
1106
|
+
"Never pass a userId or a name as a chatId.",
|
|
1107
|
+
),
|
|
1108
|
+
parameters: ClawchatGetDirectConversationSchema,
|
|
1109
|
+
async execute(_callId, params) {
|
|
1110
|
+
return await recordClawchatToolCall(accountId, "clawchat_get_direct_conversation", params, async () => {
|
|
1111
|
+
const p = params as ClawchatGetDirectConversationParams;
|
|
1112
|
+
const built = buildClient(accountId);
|
|
1113
|
+
if (!built.ok) return built.error;
|
|
1114
|
+
try {
|
|
1115
|
+
const data = await built.client.getDirectConversation(p.userId);
|
|
1116
|
+
return jsonResponse(data);
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
if (err instanceof ClawlingApiError) {
|
|
1119
|
+
return apiError(err);
|
|
1120
|
+
}
|
|
1121
|
+
return genericError(err);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
},
|
|
1125
|
+
};
|
|
1126
|
+
},
|
|
1127
|
+
{ name: "clawchat_get_direct_conversation" },
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1093
1130
|
api.registerTool(
|
|
1094
1131
|
(ctx) => {
|
|
1095
1132
|
const accountId = resolveOpenclawClawlingToolAccountId(ctx);
|
package/src/ws-client.ts
CHANGED
|
@@ -733,6 +733,10 @@ export class ClawChatClient extends EventEmitter {
|
|
|
733
733
|
if (env.event === EVENT.NOTIFY_SIGNAL) this.noteConversationSignalAlive(env);
|
|
734
734
|
if (env.event === EVENT.NOTIFY_SIGNAL) this.emit("notify:signal", env);
|
|
735
735
|
if (env.event === EVENT.REPLAY_DONE) this.emit("replay:done", env);
|
|
736
|
+
// Replay-start boundary frame (docs/client-integration.md §11.7). Control
|
|
737
|
+
// only: it carries no seq/dseq, so it is never acked and never becomes a
|
|
738
|
+
// `message`. Its own emitter keeps it out of the unknown-event path.
|
|
739
|
+
if (env.event === EVENT.HISTORY_TRUNCATED) this.emit("history:truncated", env);
|
|
736
740
|
if (env.event === EVENT.OFFLINE_DONE) this.emit("offline:done");
|
|
737
741
|
}
|
|
738
742
|
|