@clawling/clawchat-plugin-openclaw 2026.10.9-2 → 2026.10.9-4
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/README.md +1 -1
- package/dist/index.js +4 -0
- package/dist/src/clawchat-memory.js +6 -1
- package/dist/src/config.js +6 -0
- package/dist/src/host-tools-guard.js +165 -0
- package/dist/src/host-version.js +35 -0
- package/dist/src/memory-scope.js +166 -0
- package/dist/src/outbound.js +28 -2
- package/dist/src/owner-identity.js +29 -0
- package/dist/src/reply-dispatcher.js +26 -13
- package/dist/src/runtime.js +73 -12
- package/dist/src/storage.js +153 -15
- package/dist/src/terminal-send.js +69 -12
- package/dist/src/tools.js +324 -36
- package/dist/src/turn-boundary.js +68 -0
- package/index.ts +4 -0
- package/openclaw.plugin.json +4 -0
- package/package.json +3 -3
- package/src/clawchat-memory.ts +13 -0
- package/src/config.ts +11 -0
- package/src/host-tools-guard.ts +189 -0
- package/src/host-version.ts +35 -0
- package/src/memory-scope.ts +191 -0
- package/src/outbound.ts +39 -3
- package/src/owner-identity.ts +49 -0
- package/src/reply-dispatcher.ts +31 -13
- package/src/runtime.ts +81 -13
- package/src/storage.ts +176 -16
- package/src/terminal-send.ts +97 -13
- package/src/tools.ts +347 -40
- package/src/turn-boundary.ts +79 -0
package/dist/src/tools.js
CHANGED
|
@@ -8,10 +8,13 @@ import { ClawlingApiError, } from "./api-types.js";
|
|
|
8
8
|
import { mapGateOutcome } from "./gate-outcome.js";
|
|
9
9
|
import { CHANNEL_ID, normalizeOpenclawClawlingAccountId, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
10
10
|
import { clawChatDbPathForStateDir, getClawChatStore, makeOwnerProfilePersister, } from "./storage.js";
|
|
11
|
-
import { sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.js";
|
|
11
|
+
import { isPlausibleMessageId, sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.js";
|
|
12
|
+
import { isValidChatId } from "./ws-client.js";
|
|
12
13
|
import { getOpenclawClawlingClient, } from "./runtime.js";
|
|
13
14
|
import { markTerminalClawChatSend, getCurrentTerminalSendScope } from "./terminal-send.js";
|
|
14
15
|
import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
|
|
16
|
+
import { canReadMemory, memoryScopeNote, memoryScopeRefusal, resolveMemoryScope, } from "./memory-scope.js";
|
|
17
|
+
import { clawChatSessionAccountId, NON_OWNER_TOOL_ALLOWLIST, normalizeToolChatId } from "./turn-boundary.js";
|
|
15
18
|
import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
|
|
16
19
|
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, ClawchatOrchListAgentsSchema, ClawchatOrchGetAgentSchema, ClawchatOrchSetAgentBehaviorSchema, ClawchatOrchListGroupsSchema, ClawchatOrchGetGroupSchema, ClawchatOrchSetGroupPromptSchema, ClawchatOrchCreateGroupSchema, ClawchatOrchGroupMemberSchema, ClawchatOrchSetGroupAgentSettingsSchema, ClawchatOrchCreateConnectCodeSchema, ClawchatOrchGetConnectCodeSchema, } from "./tools-schema.js";
|
|
17
20
|
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
@@ -89,6 +92,15 @@ export function apiError(err) {
|
|
|
89
92
|
function validationError(message) {
|
|
90
93
|
return jsonResponse({ error: "validation", message });
|
|
91
94
|
}
|
|
95
|
+
function reactionBlockedError(chatId, reason) {
|
|
96
|
+
return jsonResponse({
|
|
97
|
+
error: "send_blocked",
|
|
98
|
+
reason,
|
|
99
|
+
message: reason === "invalid_chat_id"
|
|
100
|
+
? `clawchat-plugin-openclaw: "${chatId}" is not a ClawChat chat id (expected cnv_…); the reaction was not sent`
|
|
101
|
+
: "clawchat-plugin-openclaw: this chat was rejected by the server; the reaction was not sent",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
92
104
|
function genericError(err) {
|
|
93
105
|
return jsonResponse({
|
|
94
106
|
error: "unknown",
|
|
@@ -123,8 +135,9 @@ const MEMORY_READ_MAX_LIMIT = 100_000;
|
|
|
123
135
|
function toolDescription(...parts) {
|
|
124
136
|
return `${parts.join("")} ${DIRECT_TOOL_GUARD}`;
|
|
125
137
|
}
|
|
138
|
+
const MEMORY_SCOPE_DESCRIPTION = "In a non-owner direct chat only that person's note is readable; in a group only that group's note and its members' notes are readable. owner.md is readable only in the owner's direct chat. Replace, edit and metadata operations require a readable target; append may add facts to any target without revealing existing content.";
|
|
126
139
|
function memoryToolDescription(...parts) {
|
|
127
|
-
return parts.join("")
|
|
140
|
+
return `${parts.join("")} ${MEMORY_SCOPE_DESCRIPTION}`;
|
|
128
141
|
}
|
|
129
142
|
function inferMimeFromPath(filePath) {
|
|
130
143
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -170,12 +183,18 @@ function planMomentImages(images) {
|
|
|
170
183
|
}
|
|
171
184
|
return { ok: true, entries };
|
|
172
185
|
}
|
|
186
|
+
const MESSAGE_ID_ECHO_LIMIT = 64;
|
|
187
|
+
function notAMessageIdMessage(field, value) {
|
|
188
|
+
const shown = value.length > MESSAGE_ID_ECHO_LIMIT ? `${value.slice(0, MESSAGE_ID_ECHO_LIMIT)}…` : value;
|
|
189
|
+
return `clawchat-plugin-openclaw: ${field} "${shown}" is not a message in this chat; use the real message id from this chat's message metadata (e.g. msg-…), or omit it`;
|
|
190
|
+
}
|
|
173
191
|
function validateMentionMessageParams(params) {
|
|
174
192
|
if (!params || typeof params !== "object") {
|
|
175
193
|
return { ok: false, message: "clawchat-plugin-openclaw: params are required" };
|
|
176
194
|
}
|
|
177
195
|
const p = params;
|
|
178
|
-
|
|
196
|
+
const chatId = normalizeToolChatId(p.chatId);
|
|
197
|
+
if (!chatId) {
|
|
179
198
|
return { ok: false, message: "clawchat-plugin-openclaw: chatId is required" };
|
|
180
199
|
}
|
|
181
200
|
if (p.chatType !== undefined && p.chatType !== "group" && p.chatType !== "direct") {
|
|
@@ -211,11 +230,12 @@ function validateMentionMessageParams(params) {
|
|
|
211
230
|
ok: true,
|
|
212
231
|
params: {
|
|
213
232
|
...p,
|
|
233
|
+
chatId,
|
|
214
234
|
replyToMessageId,
|
|
215
235
|
},
|
|
216
236
|
};
|
|
217
237
|
}
|
|
218
|
-
return { ok: true, params: p };
|
|
238
|
+
return { ok: true, params: { ...p, chatId } };
|
|
219
239
|
}
|
|
220
240
|
/**
|
|
221
241
|
* Account a tool call belongs to, or `null` when the call carries no ClawChat
|
|
@@ -241,6 +261,42 @@ export function resolveOpenclawClawlingToolAccountId(ctx) {
|
|
|
241
261
|
}
|
|
242
262
|
return null;
|
|
243
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* True when the host says this tool call's run is a ClawChat run: every
|
|
266
|
+
* channel the trusted ctx names (`deliveryContext.channel`, `messageChannel`)
|
|
267
|
+
* is this channel, and at least one is present. Missing or foreign → false.
|
|
268
|
+
*/
|
|
269
|
+
export function isClawChatToolRun(ctx) {
|
|
270
|
+
const channels = [ctx.deliveryContext?.channel, ctx.messageChannel]
|
|
271
|
+
.map((channel) => (typeof channel === "string" ? channel.trim().toLowerCase() : ""))
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
return channels.length > 0 && channels.every((channel) => channel === CHANNEL_ID);
|
|
274
|
+
}
|
|
275
|
+
// Names this plugin registered through its wrapper below. The host-tool guard
|
|
276
|
+
// lets an allowlisted `clawchat_*` name through only when it is in this set,
|
|
277
|
+
// so another plugin's tool under a lookalike name is not trusted by prefix.
|
|
278
|
+
const registeredClawChatToolNames = new Set();
|
|
279
|
+
/** Snapshot of the tool names this plugin registered; callers cannot mutate the record. */
|
|
280
|
+
export function getRegisteredClawChatToolNames() {
|
|
281
|
+
return new Set(registeredClawChatToolNames);
|
|
282
|
+
}
|
|
283
|
+
// Host tool search (OpenClaw 2026.9.6+) hides every tool not marked
|
|
284
|
+
// `catalogMode: "direct-only"` behind tool_search/tool_call, whose hook ctx has
|
|
285
|
+
// no requester. The allowlisted tools stay directly callable so non-owner
|
|
286
|
+
// turns keep the full hook ctx; older hosts ignore the field.
|
|
287
|
+
function markDirectOnly(tool) {
|
|
288
|
+
return Object.assign(tool, { catalogMode: "direct-only" });
|
|
289
|
+
}
|
|
290
|
+
function directOnlyTool(tool) {
|
|
291
|
+
if (typeof tool !== "function")
|
|
292
|
+
return markDirectOnly(tool);
|
|
293
|
+
return (ctx) => {
|
|
294
|
+
const resolved = tool(ctx);
|
|
295
|
+
if (Array.isArray(resolved))
|
|
296
|
+
return resolved.map(markDirectOnly);
|
|
297
|
+
return resolved ? markDirectOnly(resolved) : resolved;
|
|
298
|
+
};
|
|
299
|
+
}
|
|
244
300
|
export function registerOpenclawClawlingTools(api, options = {}) {
|
|
245
301
|
if (!api.config) {
|
|
246
302
|
api.logger.debug?.("clawchat-plugin-openclaw: api.config missing; skipping tool registration");
|
|
@@ -250,9 +306,14 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
250
306
|
// the end is derived from what was actually registered, not hand-maintained.
|
|
251
307
|
const registeredToolNames = [];
|
|
252
308
|
const registerTool = (tool, opts) => {
|
|
253
|
-
|
|
309
|
+
const directOnly = Boolean(opts?.name && NON_OWNER_TOOL_ALLOWLIST.includes(opts.name));
|
|
310
|
+
api.registerTool(directOnly ? directOnlyTool(tool) : tool, opts);
|
|
311
|
+
// Recorded only after registerTool returned without throwing (the SDK
|
|
312
|
+
// returns void, so host-side duplicate rejection is not observable here).
|
|
313
|
+
if (opts?.name) {
|
|
254
314
|
registeredToolNames.push(opts.name);
|
|
255
|
-
|
|
315
|
+
registeredClawChatToolNames.add(opts.name);
|
|
316
|
+
}
|
|
256
317
|
};
|
|
257
318
|
// Re-resolve at call time so config reloads pick up new tokens / baseUrl.
|
|
258
319
|
function resolveCurrent(accountId) {
|
|
@@ -273,6 +334,90 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
273
334
|
return null;
|
|
274
335
|
}
|
|
275
336
|
}
|
|
337
|
+
let warnedMessageIdStore = false;
|
|
338
|
+
/**
|
|
339
|
+
* An explicit message id (react target, mention reply) must be one the
|
|
340
|
+
* plugin has stored for this chat; the stored chat id and message id (their
|
|
341
|
+
* real case) are what gets sent. When the store cannot answer, fall back to
|
|
342
|
+
* the message-id shape check rather than block every call, keep the ids as
|
|
343
|
+
* given, and warn once without ids or arguments.
|
|
344
|
+
*/
|
|
345
|
+
function resolveExplicitMessageId(input) {
|
|
346
|
+
let seen = null;
|
|
347
|
+
try {
|
|
348
|
+
const store = resolveStore();
|
|
349
|
+
seen = store?.findSeenMessageInChat
|
|
350
|
+
? store.findSeenMessageInChat({
|
|
351
|
+
platform: "openclaw",
|
|
352
|
+
accountId: input.accountId,
|
|
353
|
+
chatId: input.chatId,
|
|
354
|
+
messageId: input.value,
|
|
355
|
+
})
|
|
356
|
+
: null;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
seen = null;
|
|
360
|
+
}
|
|
361
|
+
if (seen)
|
|
362
|
+
return { ok: true, chatId: seen.chatId, messageId: seen.messageId };
|
|
363
|
+
if (seen === null) {
|
|
364
|
+
if (!warnedMessageIdStore) {
|
|
365
|
+
warnedMessageIdStore = true;
|
|
366
|
+
api.logger.warn("clawchat-plugin-openclaw: message-id store check unavailable; falling back to the message-id shape check");
|
|
367
|
+
}
|
|
368
|
+
if (isPlausibleMessageId(input.value))
|
|
369
|
+
return { ok: true, chatId: input.chatId, messageId: input.value };
|
|
370
|
+
}
|
|
371
|
+
return { ok: false, error: validationError(notAMessageIdMessage(input.field, input.value)) };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The chat id in the case the ledger already stores for it, so a send, its
|
|
375
|
+
* terminal marker and its ledger row all name the chat as the inbound path
|
|
376
|
+
* does (a second case splits the chat for memory scope). The id as given
|
|
377
|
+
* when the ledger has no row for the chat or cannot answer.
|
|
378
|
+
*/
|
|
379
|
+
function storedChatIdCase(accountId, chatId) {
|
|
380
|
+
try {
|
|
381
|
+
const stored = resolveStore()?.findStoredChatId?.({ platform: "openclaw", accountId, chatId });
|
|
382
|
+
return typeof stored === "string" && stored ? stored : chatId;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return chatId;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Best-effort ledger row for a sent mention, so its id can be reacted to or
|
|
390
|
+
* quoted later. Written after the ack; a store failure never affects the send.
|
|
391
|
+
*/
|
|
392
|
+
function recordMentionSent(input) {
|
|
393
|
+
try {
|
|
394
|
+
const store = resolveStore();
|
|
395
|
+
const claimed = store?.claimMessageOnce?.({
|
|
396
|
+
platform: "openclaw",
|
|
397
|
+
accountId: input.accountId,
|
|
398
|
+
kind: "message",
|
|
399
|
+
direction: "outbound",
|
|
400
|
+
eventType: "message.send",
|
|
401
|
+
chatId: input.chatId,
|
|
402
|
+
messageId: input.messageId,
|
|
403
|
+
text: input.text,
|
|
404
|
+
raw: { mode: "tool-mention" },
|
|
405
|
+
});
|
|
406
|
+
if (claimed !== true)
|
|
407
|
+
return;
|
|
408
|
+
store?.markMessageAcknowledged?.({
|
|
409
|
+
accountId: input.accountId,
|
|
410
|
+
kind: "message",
|
|
411
|
+
direction: "outbound",
|
|
412
|
+
messageId: input.messageId,
|
|
413
|
+
protocolMessageId: input.messageId,
|
|
414
|
+
ackedAt: input.acceptedAt ?? Date.now(),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Diagnostics only; the mention has already been sent.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
276
421
|
function detailsError(details) {
|
|
277
422
|
if (!details || typeof details !== "object")
|
|
278
423
|
return null;
|
|
@@ -452,6 +597,49 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
452
597
|
function isUnsafeMemoryPathError(message) {
|
|
453
598
|
return /Unsafe clawchat memory|Resolved clawchat memory path is outside root/i.test(message);
|
|
454
599
|
}
|
|
600
|
+
// One warning per failure class per registration; never tool arguments.
|
|
601
|
+
const scopeWarnings = new Set();
|
|
602
|
+
function warnScopeOnce(failure) {
|
|
603
|
+
if (scopeWarnings.has(failure))
|
|
604
|
+
return;
|
|
605
|
+
scopeWarnings.add(failure);
|
|
606
|
+
try {
|
|
607
|
+
api.logger.warn(`clawchat-plugin-openclaw: memory scope ${failure}; no additional notes readable`);
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// Logging must not widen or break the (restrictive) scope.
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Scope of the conversation behind this call, resolved fresh on every call
|
|
615
|
+
* (activation and group membership can change between calls). The account
|
|
616
|
+
* comes from the direct session key and/or the trusted tool context; a
|
|
617
|
+
* missing or contradictory account reads nothing.
|
|
618
|
+
*/
|
|
619
|
+
async function memoryScopeFor(ctx, root) {
|
|
620
|
+
const sessionAccount = clawChatSessionAccountId(ctx.sessionKey) ?? null;
|
|
621
|
+
const contextAccount = resolveOpenclawClawlingToolAccountId(ctx);
|
|
622
|
+
if (sessionAccount && contextAccount && sessionAccount !== contextAccount) {
|
|
623
|
+
warnScopeOnce("account-mismatch");
|
|
624
|
+
return { kind: "unknown", chatId: "", members: new Set() };
|
|
625
|
+
}
|
|
626
|
+
const accountId = sessionAccount ?? contextAccount;
|
|
627
|
+
if (!accountId)
|
|
628
|
+
return { kind: "unknown", chatId: "", members: new Set() };
|
|
629
|
+
const delivery = ctx.deliveryContext;
|
|
630
|
+
return resolveMemoryScope({
|
|
631
|
+
root,
|
|
632
|
+
ctx: {
|
|
633
|
+
sessionKey: ctx.sessionKey,
|
|
634
|
+
requesterSenderId: ctx.requesterSenderId,
|
|
635
|
+
deliveryContext: delivery?.channel === CHANNEL_ID ? { to: delivery.to } : undefined,
|
|
636
|
+
},
|
|
637
|
+
accountId,
|
|
638
|
+
configOwnerUserId: resolveCurrent(accountId).ownerUserId,
|
|
639
|
+
store: resolveStore(),
|
|
640
|
+
warn: warnScopeOnce,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
455
643
|
function memoryToolError(err) {
|
|
456
644
|
if (err instanceof ClawlingApiError)
|
|
457
645
|
return apiError(err);
|
|
@@ -478,11 +666,14 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
478
666
|
}
|
|
479
667
|
try {
|
|
480
668
|
const p = params;
|
|
481
|
-
|
|
669
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
670
|
+
const result = await searchClawChatMemory(root, {
|
|
482
671
|
query: p.query,
|
|
483
672
|
targetTypes: p.targetTypes,
|
|
484
673
|
maxResults: p.maxResults,
|
|
485
|
-
}));
|
|
674
|
+
}, { readable: (target) => canReadMemory(scope, target) });
|
|
675
|
+
const scopeNote = memoryScopeNote(scope);
|
|
676
|
+
return jsonResponse({ ...result, ...(scopeNote ? { scopeNote } : {}) });
|
|
486
677
|
}
|
|
487
678
|
catch (err) {
|
|
488
679
|
return memoryToolError(err);
|
|
@@ -507,8 +698,12 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
507
698
|
const targetError = validateTarget(root, p);
|
|
508
699
|
if (targetError)
|
|
509
700
|
return memoryToolError(new Error(targetError));
|
|
701
|
+
const target = targetFromParams(p);
|
|
702
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
703
|
+
if (!canReadMemory(scope, target))
|
|
704
|
+
return jsonResponse(memoryScopeRefusal(scope, target));
|
|
510
705
|
const { offset, limit } = normalizeReadBounds(p);
|
|
511
|
-
const file = await readClawChatMemoryFile(root,
|
|
706
|
+
const file = await readClawChatMemoryFile(root, target);
|
|
512
707
|
if (!file.exists) {
|
|
513
708
|
return jsonResponse({
|
|
514
709
|
exists: false,
|
|
@@ -553,15 +748,26 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
553
748
|
if (!root) {
|
|
554
749
|
return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
|
|
555
750
|
}
|
|
751
|
+
const p = params;
|
|
752
|
+
let readable = false;
|
|
556
753
|
try {
|
|
557
|
-
const p = params;
|
|
558
754
|
const targetError = validateTarget(root, p);
|
|
559
755
|
if (targetError)
|
|
560
756
|
return memoryToolError(new Error(targetError));
|
|
561
|
-
|
|
757
|
+
const target = targetFromParams(p);
|
|
758
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
759
|
+
readable = canReadMemory(scope, target);
|
|
760
|
+
// Replacing wipes what is there; append may go to any note.
|
|
761
|
+
if (p.mode !== "append" && !readable)
|
|
762
|
+
return jsonResponse(memoryScopeRefusal(scope, target));
|
|
763
|
+
await writeClawChatMemoryBody(root, target, p.mode, p.content);
|
|
562
764
|
return jsonResponse({ ok: true, targetType: p.targetType, targetId: p.targetId, mode: p.mode });
|
|
563
765
|
}
|
|
564
766
|
catch (err) {
|
|
767
|
+
// An unreadable note's failure text could reveal its state.
|
|
768
|
+
if (p.mode === "append" && !readable) {
|
|
769
|
+
return jsonResponse({ error: "memory_write_failed", message: "The memory append could not be completed." });
|
|
770
|
+
}
|
|
565
771
|
return memoryToolError(err);
|
|
566
772
|
}
|
|
567
773
|
},
|
|
@@ -586,7 +792,11 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
586
792
|
const targetError = validateTarget(root, p);
|
|
587
793
|
if (targetError)
|
|
588
794
|
return memoryToolError(new Error(targetError));
|
|
589
|
-
|
|
795
|
+
const target = targetFromParams(p);
|
|
796
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
797
|
+
if (!canReadMemory(scope, target))
|
|
798
|
+
return jsonResponse(memoryScopeRefusal(scope, target));
|
|
799
|
+
await editClawChatMemoryBody(root, target, p.oldText, p.newText);
|
|
590
800
|
return jsonResponse({ ok: true, targetType: p.targetType, targetId: p.targetId });
|
|
591
801
|
}
|
|
592
802
|
catch (err) {
|
|
@@ -612,14 +822,18 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
612
822
|
if (!root) {
|
|
613
823
|
return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
|
|
614
824
|
}
|
|
615
|
-
const built = buildClient(accountId);
|
|
616
|
-
if (!built.ok)
|
|
617
|
-
return built.error;
|
|
618
825
|
try {
|
|
619
826
|
const p = params;
|
|
620
827
|
const targetError = validateTarget(root, p);
|
|
621
828
|
if (targetError)
|
|
622
829
|
return memoryToolError(new Error(targetError));
|
|
830
|
+
const target = targetFromParams(p);
|
|
831
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
832
|
+
if (!canReadMemory(scope, target))
|
|
833
|
+
return jsonResponse(memoryScopeRefusal(scope, target));
|
|
834
|
+
const built = buildClient(accountId);
|
|
835
|
+
if (!built.ok)
|
|
836
|
+
return built.error;
|
|
623
837
|
const account = resolveCurrent(accountId);
|
|
624
838
|
if (p.direction === "push") {
|
|
625
839
|
if (!Array.isArray(p.fields) || p.fields.length === 0) {
|
|
@@ -683,9 +897,6 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
683
897
|
if (!root) {
|
|
684
898
|
return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
|
|
685
899
|
}
|
|
686
|
-
const built = buildClient(accountId);
|
|
687
|
-
if (!built.ok)
|
|
688
|
-
return built.error;
|
|
689
900
|
try {
|
|
690
901
|
const p = params;
|
|
691
902
|
const targetError = validateTarget(root, p);
|
|
@@ -694,6 +905,13 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
694
905
|
const patchError = validateMetadataUpdatePatch(p);
|
|
695
906
|
if (patchError)
|
|
696
907
|
return validationError(patchError);
|
|
908
|
+
const target = targetFromParams(p);
|
|
909
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
910
|
+
if (!canReadMemory(scope, target))
|
|
911
|
+
return jsonResponse(memoryScopeRefusal(scope, target));
|
|
912
|
+
const built = buildClient(accountId);
|
|
913
|
+
if (!built.ok)
|
|
914
|
+
return built.error;
|
|
697
915
|
const account = resolveCurrent(accountId);
|
|
698
916
|
if (p.targetType === "owner" && !account.agentId) {
|
|
699
917
|
return configError("clawchat-plugin-openclaw: agentId is required");
|
|
@@ -895,7 +1113,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
895
1113
|
if (!built.ok)
|
|
896
1114
|
return built.error;
|
|
897
1115
|
try {
|
|
898
|
-
const data = await built.client.getConversation(p.conversationId);
|
|
1116
|
+
const data = await built.client.getConversation(normalizeToolChatId(p.conversationId));
|
|
899
1117
|
return jsonResponse(data);
|
|
900
1118
|
}
|
|
901
1119
|
catch (err) {
|
|
@@ -957,7 +1175,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
957
1175
|
if (!built.ok)
|
|
958
1176
|
return built.error;
|
|
959
1177
|
try {
|
|
960
|
-
const data = await built.client.leaveConversation(p.conversationId);
|
|
1178
|
+
const data = await built.client.leaveConversation(normalizeToolChatId(p.conversationId));
|
|
961
1179
|
return jsonResponse(data);
|
|
962
1180
|
}
|
|
963
1181
|
catch (err) {
|
|
@@ -988,7 +1206,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
988
1206
|
if (!built.ok)
|
|
989
1207
|
return built.error;
|
|
990
1208
|
try {
|
|
991
|
-
const data = await built.client.addConversationMember(p.conversationId, p.userId);
|
|
1209
|
+
const data = await built.client.addConversationMember(normalizeToolChatId(p.conversationId), p.userId);
|
|
992
1210
|
return jsonResponse(data);
|
|
993
1211
|
}
|
|
994
1212
|
catch (err) {
|
|
@@ -1025,6 +1243,20 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1025
1243
|
}
|
|
1026
1244
|
try {
|
|
1027
1245
|
const p = validated.params;
|
|
1246
|
+
let chatId = isValidChatId(p.chatId) ? storedChatIdCase(account.accountId, p.chatId) : p.chatId;
|
|
1247
|
+
let replyToMessageId = p.replyToMessageId;
|
|
1248
|
+
if (replyToMessageId) {
|
|
1249
|
+
const resolved = resolveExplicitMessageId({
|
|
1250
|
+
field: "replyToMessageId",
|
|
1251
|
+
value: replyToMessageId,
|
|
1252
|
+
accountId: account.accountId,
|
|
1253
|
+
chatId,
|
|
1254
|
+
});
|
|
1255
|
+
if (!resolved.ok)
|
|
1256
|
+
return resolved.error;
|
|
1257
|
+
chatId = resolved.chatId;
|
|
1258
|
+
replyToMessageId = resolved.messageId;
|
|
1259
|
+
}
|
|
1028
1260
|
const client = getOpenclawClawlingClient(account.accountId);
|
|
1029
1261
|
if (!client) {
|
|
1030
1262
|
return configError("ClawChat websocket client is not ready");
|
|
@@ -1033,15 +1265,15 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1033
1265
|
client,
|
|
1034
1266
|
account,
|
|
1035
1267
|
to: {
|
|
1036
|
-
chatId
|
|
1268
|
+
chatId,
|
|
1037
1269
|
chatType: p.chatType ?? "group",
|
|
1038
1270
|
},
|
|
1039
1271
|
...(p.text !== undefined ? { text: p.text } : {}),
|
|
1040
1272
|
mentions: p.mentions,
|
|
1041
|
-
...(
|
|
1273
|
+
...(replyToMessageId
|
|
1042
1274
|
? {
|
|
1043
1275
|
replyCtx: {
|
|
1044
|
-
replyToMessageId
|
|
1276
|
+
replyToMessageId,
|
|
1045
1277
|
},
|
|
1046
1278
|
}
|
|
1047
1279
|
: {}),
|
|
@@ -1049,11 +1281,23 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1049
1281
|
});
|
|
1050
1282
|
if (!result)
|
|
1051
1283
|
return jsonResponse(result);
|
|
1052
|
-
|
|
1284
|
+
recordMentionSent({
|
|
1053
1285
|
accountId: account.accountId,
|
|
1054
|
-
chatId
|
|
1286
|
+
chatId,
|
|
1055
1287
|
messageId: result.messageId,
|
|
1288
|
+
text: p.text ?? "",
|
|
1289
|
+
...(result.acceptedAt !== undefined ? { acceptedAt: result.acceptedAt } : {}),
|
|
1056
1290
|
});
|
|
1291
|
+
// Same trust rule as the react defaults: only a ClawChat run's
|
|
1292
|
+
// turn may have its follow-up reply suppressed.
|
|
1293
|
+
if (isClawChatToolRun(ctx)) {
|
|
1294
|
+
markTerminalClawChatSend({
|
|
1295
|
+
accountId: account.accountId,
|
|
1296
|
+
chatId,
|
|
1297
|
+
messageId: result.messageId,
|
|
1298
|
+
sessionKey: ctx.sessionKey,
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1057
1301
|
return jsonResponse({
|
|
1058
1302
|
sent: true,
|
|
1059
1303
|
terminal: true,
|
|
@@ -1076,7 +1320,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1076
1320
|
label: "React to ClawChat Message",
|
|
1077
1321
|
description: toolDescription("React to a ClawChat message with a single quick emoji (the bubble long-press reaction), instead of sending a text message. " +
|
|
1078
1322
|
"TRIGGER - invoke when a short acknowledgement or emotional beat (agreement, thanks, laughter, celebration, sympathy) reads more naturally as an emoji on the message than as a sentence, e.g. a simple 👍 to \"done\" or ❤️ to good news. " +
|
|
1079
|
-
"
|
|
1323
|
+
"In the owner's direct chat, omit targetMessageId and chatId to react to the message that triggered the current turn; pass targetMessageId to react to an earlier message. In group chats and other direct chats, always pass this chat's chatId. " +
|
|
1080
1324
|
"Set remove:true to take back your reaction. " +
|
|
1081
1325
|
"Prefer the quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔. " +
|
|
1082
1326
|
"When a reaction is all you want to send, do not also send a normal text reply."),
|
|
@@ -1091,15 +1335,55 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1091
1335
|
if (!account.configured) {
|
|
1092
1336
|
return configError("clawchat-plugin-openclaw: account is not configured");
|
|
1093
1337
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1338
|
+
// Defaults come only from this call's own turn: the open turn in
|
|
1339
|
+
// its async context registered for the same session key as the
|
|
1340
|
+
// trusted tool ctx (see terminal-send.ts). Anything else fails as
|
|
1341
|
+
// a validation error asking for explicit ids.
|
|
1342
|
+
// The run must also be a ClawChat run: under a session key shared
|
|
1343
|
+
// with another channel, that channel's turns never register or
|
|
1344
|
+
// supersede a ClawChat turn, so its calls get no defaults.
|
|
1345
|
+
const scope = isClawChatToolRun(ctx)
|
|
1346
|
+
? getCurrentTerminalSendScope({ sessionKey: ctx.sessionKey })
|
|
1347
|
+
: null;
|
|
1348
|
+
// Only a missing or blank chatId defaults. A given one that strips
|
|
1349
|
+
// to nothing (`clawchat-plugin-openclaw:`, `group:`) stays as given
|
|
1350
|
+
// so outbound refuses it as invalid_chat_id instead of it silently
|
|
1351
|
+
// reaching the current turn's chat.
|
|
1352
|
+
const rawChatId = typeof p.chatId === "string" ? p.chatId.trim() : "";
|
|
1353
|
+
let chatId = rawChatId ? normalizeToolChatId(rawChatId) || rawChatId : scope?.chatId;
|
|
1354
|
+
const explicitTargetMessageId = typeof p.targetMessageId === "string" ? p.targetMessageId.trim() : "";
|
|
1355
|
+
// The default target is the current turn's message, so it applies
|
|
1356
|
+
// only to that turn's chat: chatId omitted, or naming the scope's
|
|
1357
|
+
// chat (any case, host target form). The send then goes to the
|
|
1358
|
+
// scope's chat id. In any other chat that id names no message.
|
|
1359
|
+
const defaultsToScopeChat = Boolean(scope)
|
|
1360
|
+
&& (!rawChatId || (chatId ?? "").toLowerCase() === scope.chatId.toLowerCase());
|
|
1361
|
+
let targetMessageId = explicitTargetMessageId || (defaultsToScopeChat ? scope.messageId : undefined);
|
|
1362
|
+
if (!explicitTargetMessageId && defaultsToScopeChat)
|
|
1363
|
+
chatId = scope.chatId;
|
|
1097
1364
|
if (!chatId) {
|
|
1098
1365
|
return validationError("clawchat-plugin-openclaw: chatId is required (no current-turn chat to default from)");
|
|
1099
1366
|
}
|
|
1100
1367
|
if (!targetMessageId) {
|
|
1101
1368
|
return validationError("clawchat-plugin-openclaw: targetMessageId is required (no current-turn message to default from)");
|
|
1102
1369
|
}
|
|
1370
|
+
// Only an explicit id is checked; the current-turn default is the
|
|
1371
|
+
// triggering message's own id. A chat id outbound would refuse is
|
|
1372
|
+
// reported as that refusal first.
|
|
1373
|
+
if (explicitTargetMessageId) {
|
|
1374
|
+
if (!isValidChatId(chatId))
|
|
1375
|
+
return reactionBlockedError(chatId, "invalid_chat_id");
|
|
1376
|
+
const resolved = resolveExplicitMessageId({
|
|
1377
|
+
field: "targetMessageId",
|
|
1378
|
+
value: explicitTargetMessageId,
|
|
1379
|
+
accountId: account.accountId,
|
|
1380
|
+
chatId,
|
|
1381
|
+
});
|
|
1382
|
+
if (!resolved.ok)
|
|
1383
|
+
return resolved.error;
|
|
1384
|
+
chatId = resolved.chatId;
|
|
1385
|
+
targetMessageId = resolved.messageId;
|
|
1386
|
+
}
|
|
1103
1387
|
const client = getOpenclawClawlingClient(account.accountId);
|
|
1104
1388
|
if (!client) {
|
|
1105
1389
|
return configError("ClawChat websocket client is not ready");
|
|
@@ -1107,7 +1391,11 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1107
1391
|
const emoji = p.emoji.trim();
|
|
1108
1392
|
const removed = p.remove === true;
|
|
1109
1393
|
try {
|
|
1110
|
-
sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
1394
|
+
const sent = sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
1395
|
+
// A locally refused reaction never left the plugin: report the
|
|
1396
|
+
// refusal so the model does not tell the user it reacted.
|
|
1397
|
+
if (!sent.sent)
|
|
1398
|
+
return reactionBlockedError(chatId, sent.reason);
|
|
1111
1399
|
return jsonResponse({ reacted: true, targetMessageId, emoji, removed });
|
|
1112
1400
|
}
|
|
1113
1401
|
catch (err) {
|
|
@@ -1576,7 +1864,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1576
1864
|
async execute(_callId, params) {
|
|
1577
1865
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_get_group", params, async () => {
|
|
1578
1866
|
const p = params;
|
|
1579
|
-
return await withClient(accountId, (c) => c.orchGetGroup(p.conversationId));
|
|
1867
|
+
return await withClient(accountId, (c) => c.orchGetGroup(normalizeToolChatId(p.conversationId)));
|
|
1580
1868
|
});
|
|
1581
1869
|
},
|
|
1582
1870
|
};
|
|
@@ -1599,7 +1887,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1599
1887
|
async execute(_callId, params) {
|
|
1600
1888
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_set_group_prompt", params, async () => {
|
|
1601
1889
|
const p = params;
|
|
1602
|
-
return await withClient(accountId, (c) => c.orchSetGroupPrompt(p.conversationId, p.description));
|
|
1890
|
+
return await withClient(accountId, (c) => c.orchSetGroupPrompt(normalizeToolChatId(p.conversationId), p.description));
|
|
1603
1891
|
});
|
|
1604
1892
|
},
|
|
1605
1893
|
};
|
|
@@ -1632,7 +1920,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1632
1920
|
async execute(_callId, params) {
|
|
1633
1921
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_add_group_member", params, async () => {
|
|
1634
1922
|
const p = params;
|
|
1635
|
-
return await withClient(accountId, (c) => c.orchAddGroupMember(p.conversationId, p.agentId));
|
|
1923
|
+
return await withClient(accountId, (c) => c.orchAddGroupMember(normalizeToolChatId(p.conversationId), p.agentId));
|
|
1636
1924
|
});
|
|
1637
1925
|
},
|
|
1638
1926
|
};
|
|
@@ -1648,7 +1936,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1648
1936
|
async execute(_callId, params) {
|
|
1649
1937
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_remove_group_member", params, async () => {
|
|
1650
1938
|
const p = params;
|
|
1651
|
-
return await withClient(accountId, (c) => c.orchRemoveGroupMember(p.conversationId, p.agentId));
|
|
1939
|
+
return await withClient(accountId, (c) => c.orchRemoveGroupMember(normalizeToolChatId(p.conversationId), p.agentId));
|
|
1652
1940
|
});
|
|
1653
1941
|
},
|
|
1654
1942
|
};
|
|
@@ -1666,7 +1954,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1666
1954
|
async execute(_callId, params) {
|
|
1667
1955
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_set_group_agent_settings", params, async () => {
|
|
1668
1956
|
const p = params;
|
|
1669
|
-
return await withClient(accountId, (c) => c.orchSetGroupAgentSettings(p.conversationId, p.agentId, {
|
|
1957
|
+
return await withClient(accountId, (c) => c.orchSetGroupAgentSettings(normalizeToolChatId(p.conversationId), p.agentId, {
|
|
1670
1958
|
muted: p.muted,
|
|
1671
1959
|
replyMode: p.replyMode,
|
|
1672
1960
|
batchDelaySeconds: p.batchDelaySeconds,
|