@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/src/tools.ts
CHANGED
|
@@ -22,8 +22,10 @@ import {
|
|
|
22
22
|
getClawChatStore,
|
|
23
23
|
makeOwnerProfilePersister,
|
|
24
24
|
type ClawChatStore,
|
|
25
|
+
type SeenMessage,
|
|
25
26
|
} from "./storage.ts";
|
|
26
|
-
import { sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.ts";
|
|
27
|
+
import { isPlausibleMessageId, sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.ts";
|
|
28
|
+
import { isValidChatId } from "./ws-client.ts";
|
|
27
29
|
import {
|
|
28
30
|
getOpenclawClawlingClient,
|
|
29
31
|
} from "./runtime.ts";
|
|
@@ -36,6 +38,14 @@ import {
|
|
|
36
38
|
writeClawChatMemoryBody,
|
|
37
39
|
type ClawChatMemoryTargetType,
|
|
38
40
|
} from "./clawchat-memory.ts";
|
|
41
|
+
import {
|
|
42
|
+
canReadMemory,
|
|
43
|
+
memoryScopeNote,
|
|
44
|
+
memoryScopeRefusal,
|
|
45
|
+
resolveMemoryScope,
|
|
46
|
+
type MemoryScope,
|
|
47
|
+
} from "./memory-scope.ts";
|
|
48
|
+
import { clawChatSessionAccountId, NON_OWNER_TOOL_ALLOWLIST, normalizeToolChatId } from "./turn-boundary.ts";
|
|
39
49
|
import {
|
|
40
50
|
pullGroupMetadata,
|
|
41
51
|
pullOwnerMetadata,
|
|
@@ -215,6 +225,16 @@ function validationError(message: string) {
|
|
|
215
225
|
return jsonResponse({ error: "validation", message });
|
|
216
226
|
}
|
|
217
227
|
|
|
228
|
+
function reactionBlockedError(chatId: string, reason: "invalid_chat_id" | "chat_rejected_by_server") {
|
|
229
|
+
return jsonResponse({
|
|
230
|
+
error: "send_blocked",
|
|
231
|
+
reason,
|
|
232
|
+
message: reason === "invalid_chat_id"
|
|
233
|
+
? `clawchat-plugin-openclaw: "${chatId}" is not a ClawChat chat id (expected cnv_…); the reaction was not sent`
|
|
234
|
+
: "clawchat-plugin-openclaw: this chat was rejected by the server; the reaction was not sent",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
218
238
|
function genericError(err: unknown) {
|
|
219
239
|
return jsonResponse({
|
|
220
240
|
error: "unknown",
|
|
@@ -254,14 +274,26 @@ const MEMORY_READ_MAX_LIMIT = 100_000;
|
|
|
254
274
|
type ToolStore = Pick<
|
|
255
275
|
ClawChatStore,
|
|
256
276
|
"recordToolCall"
|
|
257
|
-
|
|
277
|
+
> &
|
|
278
|
+
Partial<Pick<
|
|
279
|
+
ClawChatStore,
|
|
280
|
+
| "lookupActivationIdentity"
|
|
281
|
+
| "listRecentGroupMessages"
|
|
282
|
+
| "findSeenMessageInChat"
|
|
283
|
+
| "findStoredChatId"
|
|
284
|
+
| "claimMessageOnce"
|
|
285
|
+
| "markMessageAcknowledged"
|
|
286
|
+
>>;
|
|
258
287
|
|
|
259
288
|
function toolDescription(...parts: string[]): string {
|
|
260
289
|
return `${parts.join("")} ${DIRECT_TOOL_GUARD}`;
|
|
261
290
|
}
|
|
262
291
|
|
|
292
|
+
const MEMORY_SCOPE_DESCRIPTION =
|
|
293
|
+
"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.";
|
|
294
|
+
|
|
263
295
|
function memoryToolDescription(...parts: string[]): string {
|
|
264
|
-
return parts.join("")
|
|
296
|
+
return `${parts.join("")} ${MEMORY_SCOPE_DESCRIPTION}`;
|
|
265
297
|
}
|
|
266
298
|
|
|
267
299
|
function inferMimeFromPath(filePath: string): string {
|
|
@@ -313,6 +345,13 @@ function planMomentImages(
|
|
|
313
345
|
return { ok: true, entries };
|
|
314
346
|
}
|
|
315
347
|
|
|
348
|
+
const MESSAGE_ID_ECHO_LIMIT = 64;
|
|
349
|
+
|
|
350
|
+
function notAMessageIdMessage(field: string, value: string): string {
|
|
351
|
+
const shown = value.length > MESSAGE_ID_ECHO_LIMIT ? `${value.slice(0, MESSAGE_ID_ECHO_LIMIT)}…` : value;
|
|
352
|
+
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`;
|
|
353
|
+
}
|
|
354
|
+
|
|
316
355
|
function validateMentionMessageParams(params: unknown):
|
|
317
356
|
| { ok: true; params: ClawchatMentionMessageParams }
|
|
318
357
|
| { ok: false; message: string } {
|
|
@@ -320,7 +359,8 @@ function validateMentionMessageParams(params: unknown):
|
|
|
320
359
|
return { ok: false, message: "clawchat-plugin-openclaw: params are required" };
|
|
321
360
|
}
|
|
322
361
|
const p = params as Partial<ClawchatMentionMessageParams>;
|
|
323
|
-
|
|
362
|
+
const chatId = normalizeToolChatId(p.chatId);
|
|
363
|
+
if (!chatId) {
|
|
324
364
|
return { ok: false, message: "clawchat-plugin-openclaw: chatId is required" };
|
|
325
365
|
}
|
|
326
366
|
if (p.chatType !== undefined && p.chatType !== "group" && p.chatType !== "direct") {
|
|
@@ -356,11 +396,12 @@ function validateMentionMessageParams(params: unknown):
|
|
|
356
396
|
ok: true,
|
|
357
397
|
params: {
|
|
358
398
|
...p,
|
|
399
|
+
chatId,
|
|
359
400
|
replyToMessageId,
|
|
360
401
|
} as ClawchatMentionMessageParams,
|
|
361
402
|
};
|
|
362
403
|
}
|
|
363
|
-
return { ok: true, params: p as ClawchatMentionMessageParams };
|
|
404
|
+
return { ok: true, params: { ...p, chatId } as ClawchatMentionMessageParams };
|
|
364
405
|
}
|
|
365
406
|
|
|
366
407
|
/**
|
|
@@ -392,6 +433,48 @@ export function resolveOpenclawClawlingToolAccountId(ctx: OpenClawPluginToolCont
|
|
|
392
433
|
return null;
|
|
393
434
|
}
|
|
394
435
|
|
|
436
|
+
/**
|
|
437
|
+
* True when the host says this tool call's run is a ClawChat run: every
|
|
438
|
+
* channel the trusted ctx names (`deliveryContext.channel`, `messageChannel`)
|
|
439
|
+
* is this channel, and at least one is present. Missing or foreign → false.
|
|
440
|
+
*/
|
|
441
|
+
export function isClawChatToolRun(ctx: OpenClawPluginToolContext): boolean {
|
|
442
|
+
const channels = [ctx.deliveryContext?.channel, ctx.messageChannel]
|
|
443
|
+
.map((channel) => (typeof channel === "string" ? channel.trim().toLowerCase() : ""))
|
|
444
|
+
.filter(Boolean);
|
|
445
|
+
return channels.length > 0 && channels.every((channel) => channel === CHANNEL_ID);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Names this plugin registered through its wrapper below. The host-tool guard
|
|
449
|
+
// lets an allowlisted `clawchat_*` name through only when it is in this set,
|
|
450
|
+
// so another plugin's tool under a lookalike name is not trusted by prefix.
|
|
451
|
+
const registeredClawChatToolNames = new Set<string>();
|
|
452
|
+
|
|
453
|
+
/** Snapshot of the tool names this plugin registered; callers cannot mutate the record. */
|
|
454
|
+
export function getRegisteredClawChatToolNames(): ReadonlySet<string> {
|
|
455
|
+
return new Set(registeredClawChatToolNames);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type RegistrableTool = Parameters<OpenClawPluginApi["registerTool"]>[0];
|
|
459
|
+
type PluginTool = Exclude<RegistrableTool, (...args: never[]) => unknown>;
|
|
460
|
+
|
|
461
|
+
// Host tool search (OpenClaw 2026.9.6+) hides every tool not marked
|
|
462
|
+
// `catalogMode: "direct-only"` behind tool_search/tool_call, whose hook ctx has
|
|
463
|
+
// no requester. The allowlisted tools stay directly callable so non-owner
|
|
464
|
+
// turns keep the full hook ctx; older hosts ignore the field.
|
|
465
|
+
function markDirectOnly(tool: PluginTool): PluginTool {
|
|
466
|
+
return Object.assign(tool, { catalogMode: "direct-only" as const });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function directOnlyTool(tool: RegistrableTool): RegistrableTool {
|
|
470
|
+
if (typeof tool !== "function") return markDirectOnly(tool);
|
|
471
|
+
return (ctx) => {
|
|
472
|
+
const resolved = tool(ctx);
|
|
473
|
+
if (Array.isArray(resolved)) return resolved.map(markDirectOnly);
|
|
474
|
+
return resolved ? markDirectOnly(resolved) : resolved;
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
395
478
|
export function registerOpenclawClawlingTools(
|
|
396
479
|
api: OpenClawPluginApi,
|
|
397
480
|
options: { store?: ToolStore | null } = {},
|
|
@@ -405,8 +488,14 @@ export function registerOpenclawClawlingTools(
|
|
|
405
488
|
// the end is derived from what was actually registered, not hand-maintained.
|
|
406
489
|
const registeredToolNames: string[] = [];
|
|
407
490
|
const registerTool: OpenClawPluginApi["registerTool"] = (tool, opts) => {
|
|
408
|
-
|
|
409
|
-
api.registerTool(tool, opts);
|
|
491
|
+
const directOnly = Boolean(opts?.name && NON_OWNER_TOOL_ALLOWLIST.includes(opts.name));
|
|
492
|
+
api.registerTool(directOnly ? directOnlyTool(tool) : tool, opts);
|
|
493
|
+
// Recorded only after registerTool returned without throwing (the SDK
|
|
494
|
+
// returns void, so host-side duplicate rejection is not observable here).
|
|
495
|
+
if (opts?.name) {
|
|
496
|
+
registeredToolNames.push(opts.name);
|
|
497
|
+
registeredClawChatToolNames.add(opts.name);
|
|
498
|
+
}
|
|
410
499
|
};
|
|
411
500
|
|
|
412
501
|
// Re-resolve at call time so config reloads pick up new tokens / baseUrl.
|
|
@@ -428,6 +517,94 @@ export function registerOpenclawClawlingTools(
|
|
|
428
517
|
}
|
|
429
518
|
}
|
|
430
519
|
|
|
520
|
+
let warnedMessageIdStore = false;
|
|
521
|
+
/**
|
|
522
|
+
* An explicit message id (react target, mention reply) must be one the
|
|
523
|
+
* plugin has stored for this chat; the stored chat id and message id (their
|
|
524
|
+
* real case) are what gets sent. When the store cannot answer, fall back to
|
|
525
|
+
* the message-id shape check rather than block every call, keep the ids as
|
|
526
|
+
* given, and warn once without ids or arguments.
|
|
527
|
+
*/
|
|
528
|
+
function resolveExplicitMessageId(input: {
|
|
529
|
+
field: "targetMessageId" | "replyToMessageId";
|
|
530
|
+
value: string;
|
|
531
|
+
accountId: string;
|
|
532
|
+
chatId: string;
|
|
533
|
+
}): { ok: true; chatId: string; messageId: string } | { ok: false; error: ReturnType<typeof validationError> } {
|
|
534
|
+
let seen: SeenMessage | false | null = null;
|
|
535
|
+
try {
|
|
536
|
+
const store = resolveStore();
|
|
537
|
+
seen = store?.findSeenMessageInChat
|
|
538
|
+
? store.findSeenMessageInChat({
|
|
539
|
+
platform: "openclaw",
|
|
540
|
+
accountId: input.accountId,
|
|
541
|
+
chatId: input.chatId,
|
|
542
|
+
messageId: input.value,
|
|
543
|
+
})
|
|
544
|
+
: null;
|
|
545
|
+
} catch {
|
|
546
|
+
seen = null;
|
|
547
|
+
}
|
|
548
|
+
if (seen) return { ok: true, chatId: seen.chatId, messageId: seen.messageId };
|
|
549
|
+
if (seen === null) {
|
|
550
|
+
if (!warnedMessageIdStore) {
|
|
551
|
+
warnedMessageIdStore = true;
|
|
552
|
+
api.logger.warn(
|
|
553
|
+
"clawchat-plugin-openclaw: message-id store check unavailable; falling back to the message-id shape check",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (isPlausibleMessageId(input.value)) return { ok: true, chatId: input.chatId, messageId: input.value };
|
|
557
|
+
}
|
|
558
|
+
return { ok: false, error: validationError(notAMessageIdMessage(input.field, input.value)) };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The chat id in the case the ledger already stores for it, so a send, its
|
|
563
|
+
* terminal marker and its ledger row all name the chat as the inbound path
|
|
564
|
+
* does (a second case splits the chat for memory scope). The id as given
|
|
565
|
+
* when the ledger has no row for the chat or cannot answer.
|
|
566
|
+
*/
|
|
567
|
+
function storedChatIdCase(accountId: string, chatId: string): string {
|
|
568
|
+
try {
|
|
569
|
+
const stored = resolveStore()?.findStoredChatId?.({ platform: "openclaw", accountId, chatId });
|
|
570
|
+
return typeof stored === "string" && stored ? stored : chatId;
|
|
571
|
+
} catch {
|
|
572
|
+
return chatId;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Best-effort ledger row for a sent mention, so its id can be reacted to or
|
|
578
|
+
* quoted later. Written after the ack; a store failure never affects the send.
|
|
579
|
+
*/
|
|
580
|
+
function recordMentionSent(input: { accountId: string; chatId: string; messageId: string; text: string; acceptedAt?: number }): void {
|
|
581
|
+
try {
|
|
582
|
+
const store = resolveStore();
|
|
583
|
+
const claimed = store?.claimMessageOnce?.({
|
|
584
|
+
platform: "openclaw",
|
|
585
|
+
accountId: input.accountId,
|
|
586
|
+
kind: "message",
|
|
587
|
+
direction: "outbound",
|
|
588
|
+
eventType: "message.send",
|
|
589
|
+
chatId: input.chatId,
|
|
590
|
+
messageId: input.messageId,
|
|
591
|
+
text: input.text,
|
|
592
|
+
raw: { mode: "tool-mention" },
|
|
593
|
+
});
|
|
594
|
+
if (claimed !== true) return;
|
|
595
|
+
store?.markMessageAcknowledged?.({
|
|
596
|
+
accountId: input.accountId,
|
|
597
|
+
kind: "message",
|
|
598
|
+
direction: "outbound",
|
|
599
|
+
messageId: input.messageId,
|
|
600
|
+
protocolMessageId: input.messageId,
|
|
601
|
+
ackedAt: input.acceptedAt ?? Date.now(),
|
|
602
|
+
});
|
|
603
|
+
} catch {
|
|
604
|
+
// Diagnostics only; the mention has already been sent.
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
431
608
|
function detailsError(details: unknown): string | null {
|
|
432
609
|
if (!details || typeof details !== "object") return null;
|
|
433
610
|
const raw = details as { error?: unknown; message?: unknown };
|
|
@@ -636,6 +813,48 @@ export function registerOpenclawClawlingTools(
|
|
|
636
813
|
return /Unsafe clawchat memory|Resolved clawchat memory path is outside root/i.test(message);
|
|
637
814
|
}
|
|
638
815
|
|
|
816
|
+
// One warning per failure class per registration; never tool arguments.
|
|
817
|
+
const scopeWarnings = new Set<string>();
|
|
818
|
+
function warnScopeOnce(failure: string): void {
|
|
819
|
+
if (scopeWarnings.has(failure)) return;
|
|
820
|
+
scopeWarnings.add(failure);
|
|
821
|
+
try {
|
|
822
|
+
api.logger.warn(`clawchat-plugin-openclaw: memory scope ${failure}; no additional notes readable`);
|
|
823
|
+
} catch {
|
|
824
|
+
// Logging must not widen or break the (restrictive) scope.
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Scope of the conversation behind this call, resolved fresh on every call
|
|
830
|
+
* (activation and group membership can change between calls). The account
|
|
831
|
+
* comes from the direct session key and/or the trusted tool context; a
|
|
832
|
+
* missing or contradictory account reads nothing.
|
|
833
|
+
*/
|
|
834
|
+
async function memoryScopeFor(ctx: OpenClawPluginToolContext, root: string): Promise<MemoryScope> {
|
|
835
|
+
const sessionAccount = clawChatSessionAccountId(ctx.sessionKey) ?? null;
|
|
836
|
+
const contextAccount = resolveOpenclawClawlingToolAccountId(ctx);
|
|
837
|
+
if (sessionAccount && contextAccount && sessionAccount !== contextAccount) {
|
|
838
|
+
warnScopeOnce("account-mismatch");
|
|
839
|
+
return { kind: "unknown", chatId: "", members: new Set() };
|
|
840
|
+
}
|
|
841
|
+
const accountId = sessionAccount ?? contextAccount;
|
|
842
|
+
if (!accountId) return { kind: "unknown", chatId: "", members: new Set() };
|
|
843
|
+
const delivery = ctx.deliveryContext;
|
|
844
|
+
return resolveMemoryScope({
|
|
845
|
+
root,
|
|
846
|
+
ctx: {
|
|
847
|
+
sessionKey: ctx.sessionKey,
|
|
848
|
+
requesterSenderId: ctx.requesterSenderId,
|
|
849
|
+
deliveryContext: delivery?.channel === CHANNEL_ID ? { to: delivery.to } : undefined,
|
|
850
|
+
},
|
|
851
|
+
accountId,
|
|
852
|
+
configOwnerUserId: resolveCurrent(accountId).ownerUserId,
|
|
853
|
+
store: resolveStore(),
|
|
854
|
+
warn: warnScopeOnce,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
639
858
|
function memoryToolError(err: unknown) {
|
|
640
859
|
if (err instanceof ClawlingApiError) return apiError(err);
|
|
641
860
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -665,13 +884,14 @@ export function registerOpenclawClawlingTools(
|
|
|
665
884
|
}
|
|
666
885
|
try {
|
|
667
886
|
const p = params as ClawchatMemorySearchParams;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
);
|
|
887
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
888
|
+
const result = await searchClawChatMemory(root, {
|
|
889
|
+
query: p.query,
|
|
890
|
+
targetTypes: p.targetTypes,
|
|
891
|
+
maxResults: p.maxResults,
|
|
892
|
+
}, { readable: (target) => canReadMemory(scope, target) });
|
|
893
|
+
const scopeNote = memoryScopeNote(scope);
|
|
894
|
+
return jsonResponse({ ...result, ...(scopeNote ? { scopeNote } : {}) });
|
|
675
895
|
} catch (err) {
|
|
676
896
|
return memoryToolError(err);
|
|
677
897
|
}
|
|
@@ -700,8 +920,11 @@ export function registerOpenclawClawlingTools(
|
|
|
700
920
|
const p = params as ClawchatMemoryReadParams;
|
|
701
921
|
const targetError = validateTarget(root, p);
|
|
702
922
|
if (targetError) return memoryToolError(new Error(targetError));
|
|
923
|
+
const target = targetFromParams(p);
|
|
924
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
925
|
+
if (!canReadMemory(scope, target)) return jsonResponse(memoryScopeRefusal(scope, target));
|
|
703
926
|
const { offset, limit } = normalizeReadBounds(p);
|
|
704
|
-
const file = await readClawChatMemoryFile(root,
|
|
927
|
+
const file = await readClawChatMemoryFile(root, target);
|
|
705
928
|
if (!file.exists) {
|
|
706
929
|
return jsonResponse({
|
|
707
930
|
exists: false,
|
|
@@ -751,13 +974,23 @@ export function registerOpenclawClawlingTools(
|
|
|
751
974
|
if (!root) {
|
|
752
975
|
return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
|
|
753
976
|
}
|
|
977
|
+
const p = params as ClawchatMemoryWriteParams;
|
|
978
|
+
let readable = false;
|
|
754
979
|
try {
|
|
755
|
-
const p = params as ClawchatMemoryWriteParams;
|
|
756
980
|
const targetError = validateTarget(root, p);
|
|
757
981
|
if (targetError) return memoryToolError(new Error(targetError));
|
|
758
|
-
|
|
982
|
+
const target = targetFromParams(p);
|
|
983
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
984
|
+
readable = canReadMemory(scope, target);
|
|
985
|
+
// Replacing wipes what is there; append may go to any note.
|
|
986
|
+
if (p.mode !== "append" && !readable) return jsonResponse(memoryScopeRefusal(scope, target));
|
|
987
|
+
await writeClawChatMemoryBody(root, target, p.mode, p.content);
|
|
759
988
|
return jsonResponse({ ok: true, targetType: p.targetType, targetId: p.targetId, mode: p.mode });
|
|
760
989
|
} catch (err) {
|
|
990
|
+
// An unreadable note's failure text could reveal its state.
|
|
991
|
+
if (p.mode === "append" && !readable) {
|
|
992
|
+
return jsonResponse({ error: "memory_write_failed", message: "The memory append could not be completed." });
|
|
993
|
+
}
|
|
761
994
|
return memoryToolError(err);
|
|
762
995
|
}
|
|
763
996
|
},
|
|
@@ -787,7 +1020,10 @@ export function registerOpenclawClawlingTools(
|
|
|
787
1020
|
const p = params as ClawchatMemoryEditParams;
|
|
788
1021
|
const targetError = validateTarget(root, p);
|
|
789
1022
|
if (targetError) return memoryToolError(new Error(targetError));
|
|
790
|
-
|
|
1023
|
+
const target = targetFromParams(p);
|
|
1024
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
1025
|
+
if (!canReadMemory(scope, target)) return jsonResponse(memoryScopeRefusal(scope, target));
|
|
1026
|
+
await editClawChatMemoryBody(root, target, p.oldText, p.newText);
|
|
791
1027
|
return jsonResponse({ ok: true, targetType: p.targetType, targetId: p.targetId });
|
|
792
1028
|
} catch (err) {
|
|
793
1029
|
return memoryToolError(err);
|
|
@@ -818,12 +1054,15 @@ export function registerOpenclawClawlingTools(
|
|
|
818
1054
|
if (!root) {
|
|
819
1055
|
return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
|
|
820
1056
|
}
|
|
821
|
-
const built = buildClient(accountId);
|
|
822
|
-
if (!built.ok) return built.error;
|
|
823
1057
|
try {
|
|
824
1058
|
const p = params as ClawchatMetadataSyncParams;
|
|
825
1059
|
const targetError = validateTarget(root, p);
|
|
826
1060
|
if (targetError) return memoryToolError(new Error(targetError));
|
|
1061
|
+
const target = targetFromParams(p);
|
|
1062
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
1063
|
+
if (!canReadMemory(scope, target)) return jsonResponse(memoryScopeRefusal(scope, target));
|
|
1064
|
+
const built = buildClient(accountId);
|
|
1065
|
+
if (!built.ok) return built.error;
|
|
827
1066
|
const account = resolveCurrent(accountId);
|
|
828
1067
|
if (p.direction === "push") {
|
|
829
1068
|
if (!Array.isArray(p.fields) || p.fields.length === 0) {
|
|
@@ -899,14 +1138,17 @@ export function registerOpenclawClawlingTools(
|
|
|
899
1138
|
if (!root) {
|
|
900
1139
|
return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
|
|
901
1140
|
}
|
|
902
|
-
const built = buildClient(accountId);
|
|
903
|
-
if (!built.ok) return built.error;
|
|
904
1141
|
try {
|
|
905
1142
|
const p = params as ClawchatMetadataUpdateParams;
|
|
906
1143
|
const targetError = validateTarget(root, p);
|
|
907
1144
|
if (targetError) return memoryToolError(new Error(targetError));
|
|
908
1145
|
const patchError = validateMetadataUpdatePatch(p);
|
|
909
1146
|
if (patchError) return validationError(patchError);
|
|
1147
|
+
const target = targetFromParams(p);
|
|
1148
|
+
const scope = await memoryScopeFor(ctx, root);
|
|
1149
|
+
if (!canReadMemory(scope, target)) return jsonResponse(memoryScopeRefusal(scope, target));
|
|
1150
|
+
const built = buildClient(accountId);
|
|
1151
|
+
if (!built.ok) return built.error;
|
|
910
1152
|
const account = resolveCurrent(accountId);
|
|
911
1153
|
if (p.targetType === "owner" && !account.agentId) {
|
|
912
1154
|
return configError("clawchat-plugin-openclaw: agentId is required");
|
|
@@ -1175,7 +1417,7 @@ export function registerOpenclawClawlingTools(
|
|
|
1175
1417
|
const built = buildClient(accountId);
|
|
1176
1418
|
if (!built.ok) return built.error;
|
|
1177
1419
|
try {
|
|
1178
|
-
const data = await built.client.getConversation(p.conversationId);
|
|
1420
|
+
const data = await built.client.getConversation(normalizeToolChatId(p.conversationId));
|
|
1179
1421
|
return jsonResponse(data);
|
|
1180
1422
|
} catch (err) {
|
|
1181
1423
|
if (err instanceof ClawlingApiError) {
|
|
@@ -1245,7 +1487,7 @@ export function registerOpenclawClawlingTools(
|
|
|
1245
1487
|
const built = buildClient(accountId);
|
|
1246
1488
|
if (!built.ok) return built.error;
|
|
1247
1489
|
try {
|
|
1248
|
-
const data = await built.client.leaveConversation(p.conversationId);
|
|
1490
|
+
const data = await built.client.leaveConversation(normalizeToolChatId(p.conversationId));
|
|
1249
1491
|
return jsonResponse(data);
|
|
1250
1492
|
} catch (err) {
|
|
1251
1493
|
if (err instanceof ClawlingApiError) {
|
|
@@ -1280,7 +1522,7 @@ export function registerOpenclawClawlingTools(
|
|
|
1280
1522
|
const built = buildClient(accountId);
|
|
1281
1523
|
if (!built.ok) return built.error;
|
|
1282
1524
|
try {
|
|
1283
|
-
const data = await built.client.addConversationMember(p.conversationId, p.userId);
|
|
1525
|
+
const data = await built.client.addConversationMember(normalizeToolChatId(p.conversationId), p.userId);
|
|
1284
1526
|
return jsonResponse(data);
|
|
1285
1527
|
} catch (err) {
|
|
1286
1528
|
if (err instanceof ClawlingApiError) {
|
|
@@ -1321,6 +1563,19 @@ export function registerOpenclawClawlingTools(
|
|
|
1321
1563
|
}
|
|
1322
1564
|
try {
|
|
1323
1565
|
const p = validated.params;
|
|
1566
|
+
let chatId = isValidChatId(p.chatId) ? storedChatIdCase(account.accountId, p.chatId) : p.chatId;
|
|
1567
|
+
let replyToMessageId = p.replyToMessageId;
|
|
1568
|
+
if (replyToMessageId) {
|
|
1569
|
+
const resolved = resolveExplicitMessageId({
|
|
1570
|
+
field: "replyToMessageId",
|
|
1571
|
+
value: replyToMessageId,
|
|
1572
|
+
accountId: account.accountId,
|
|
1573
|
+
chatId,
|
|
1574
|
+
});
|
|
1575
|
+
if (!resolved.ok) return resolved.error;
|
|
1576
|
+
chatId = resolved.chatId;
|
|
1577
|
+
replyToMessageId = resolved.messageId;
|
|
1578
|
+
}
|
|
1324
1579
|
const client = getOpenclawClawlingClient(account.accountId);
|
|
1325
1580
|
if (!client) {
|
|
1326
1581
|
return configError("ClawChat websocket client is not ready");
|
|
@@ -1329,26 +1584,38 @@ export function registerOpenclawClawlingTools(
|
|
|
1329
1584
|
client,
|
|
1330
1585
|
account,
|
|
1331
1586
|
to: {
|
|
1332
|
-
chatId
|
|
1587
|
+
chatId,
|
|
1333
1588
|
chatType: p.chatType ?? "group",
|
|
1334
1589
|
},
|
|
1335
1590
|
...(p.text !== undefined ? { text: p.text } : {}),
|
|
1336
1591
|
mentions: p.mentions,
|
|
1337
|
-
...(
|
|
1592
|
+
...(replyToMessageId
|
|
1338
1593
|
? {
|
|
1339
1594
|
replyCtx: {
|
|
1340
|
-
replyToMessageId
|
|
1595
|
+
replyToMessageId,
|
|
1341
1596
|
},
|
|
1342
1597
|
}
|
|
1343
1598
|
: {}),
|
|
1344
1599
|
log: api.logger,
|
|
1345
1600
|
});
|
|
1346
1601
|
if (!result) return jsonResponse(result);
|
|
1347
|
-
|
|
1602
|
+
recordMentionSent({
|
|
1348
1603
|
accountId: account.accountId,
|
|
1349
|
-
chatId
|
|
1604
|
+
chatId,
|
|
1350
1605
|
messageId: result.messageId,
|
|
1606
|
+
text: p.text ?? "",
|
|
1607
|
+
...(result.acceptedAt !== undefined ? { acceptedAt: result.acceptedAt } : {}),
|
|
1351
1608
|
});
|
|
1609
|
+
// Same trust rule as the react defaults: only a ClawChat run's
|
|
1610
|
+
// turn may have its follow-up reply suppressed.
|
|
1611
|
+
if (isClawChatToolRun(ctx)) {
|
|
1612
|
+
markTerminalClawChatSend({
|
|
1613
|
+
accountId: account.accountId,
|
|
1614
|
+
chatId,
|
|
1615
|
+
messageId: result.messageId,
|
|
1616
|
+
sessionKey: ctx.sessionKey,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1352
1619
|
return jsonResponse({
|
|
1353
1620
|
sent: true,
|
|
1354
1621
|
terminal: true,
|
|
@@ -1376,7 +1643,7 @@ export function registerOpenclawClawlingTools(
|
|
|
1376
1643
|
description: toolDescription(
|
|
1377
1644
|
"React to a ClawChat message with a single quick emoji (the bubble long-press reaction), instead of sending a text message. " +
|
|
1378
1645
|
"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. " +
|
|
1379
|
-
"
|
|
1646
|
+
"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. " +
|
|
1380
1647
|
"Set remove:true to take back your reaction. " +
|
|
1381
1648
|
"Prefer the quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔. " +
|
|
1382
1649
|
"When a reaction is all you want to send, do not also send a normal text reply.",
|
|
@@ -1392,9 +1659,31 @@ export function registerOpenclawClawlingTools(
|
|
|
1392
1659
|
if (!account.configured) {
|
|
1393
1660
|
return configError("clawchat-plugin-openclaw: account is not configured");
|
|
1394
1661
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1662
|
+
// Defaults come only from this call's own turn: the open turn in
|
|
1663
|
+
// its async context registered for the same session key as the
|
|
1664
|
+
// trusted tool ctx (see terminal-send.ts). Anything else fails as
|
|
1665
|
+
// a validation error asking for explicit ids.
|
|
1666
|
+
// The run must also be a ClawChat run: under a session key shared
|
|
1667
|
+
// with another channel, that channel's turns never register or
|
|
1668
|
+
// supersede a ClawChat turn, so its calls get no defaults.
|
|
1669
|
+
const scope = isClawChatToolRun(ctx)
|
|
1670
|
+
? getCurrentTerminalSendScope({ sessionKey: ctx.sessionKey })
|
|
1671
|
+
: null;
|
|
1672
|
+
// Only a missing or blank chatId defaults. A given one that strips
|
|
1673
|
+
// to nothing (`clawchat-plugin-openclaw:`, `group:`) stays as given
|
|
1674
|
+
// so outbound refuses it as invalid_chat_id instead of it silently
|
|
1675
|
+
// reaching the current turn's chat.
|
|
1676
|
+
const rawChatId = typeof p.chatId === "string" ? p.chatId.trim() : "";
|
|
1677
|
+
let chatId = rawChatId ? normalizeToolChatId(rawChatId) || rawChatId : scope?.chatId;
|
|
1678
|
+
const explicitTargetMessageId = typeof p.targetMessageId === "string" ? p.targetMessageId.trim() : "";
|
|
1679
|
+
// The default target is the current turn's message, so it applies
|
|
1680
|
+
// only to that turn's chat: chatId omitted, or naming the scope's
|
|
1681
|
+
// chat (any case, host target form). The send then goes to the
|
|
1682
|
+
// scope's chat id. In any other chat that id names no message.
|
|
1683
|
+
const defaultsToScopeChat = Boolean(scope)
|
|
1684
|
+
&& (!rawChatId || (chatId ?? "").toLowerCase() === scope!.chatId.toLowerCase());
|
|
1685
|
+
let targetMessageId = explicitTargetMessageId || (defaultsToScopeChat ? scope!.messageId : undefined);
|
|
1686
|
+
if (!explicitTargetMessageId && defaultsToScopeChat) chatId = scope!.chatId;
|
|
1398
1687
|
if (!chatId) {
|
|
1399
1688
|
return validationError(
|
|
1400
1689
|
"clawchat-plugin-openclaw: chatId is required (no current-turn chat to default from)",
|
|
@@ -1405,6 +1694,21 @@ export function registerOpenclawClawlingTools(
|
|
|
1405
1694
|
"clawchat-plugin-openclaw: targetMessageId is required (no current-turn message to default from)",
|
|
1406
1695
|
);
|
|
1407
1696
|
}
|
|
1697
|
+
// Only an explicit id is checked; the current-turn default is the
|
|
1698
|
+
// triggering message's own id. A chat id outbound would refuse is
|
|
1699
|
+
// reported as that refusal first.
|
|
1700
|
+
if (explicitTargetMessageId) {
|
|
1701
|
+
if (!isValidChatId(chatId)) return reactionBlockedError(chatId, "invalid_chat_id");
|
|
1702
|
+
const resolved = resolveExplicitMessageId({
|
|
1703
|
+
field: "targetMessageId",
|
|
1704
|
+
value: explicitTargetMessageId,
|
|
1705
|
+
accountId: account.accountId,
|
|
1706
|
+
chatId,
|
|
1707
|
+
});
|
|
1708
|
+
if (!resolved.ok) return resolved.error;
|
|
1709
|
+
chatId = resolved.chatId;
|
|
1710
|
+
targetMessageId = resolved.messageId;
|
|
1711
|
+
}
|
|
1408
1712
|
const client = getOpenclawClawlingClient(account.accountId);
|
|
1409
1713
|
if (!client) {
|
|
1410
1714
|
return configError("ClawChat websocket client is not ready");
|
|
@@ -1412,7 +1716,10 @@ export function registerOpenclawClawlingTools(
|
|
|
1412
1716
|
const emoji = p.emoji.trim();
|
|
1413
1717
|
const removed = p.remove === true;
|
|
1414
1718
|
try {
|
|
1415
|
-
sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
1719
|
+
const sent = sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
1720
|
+
// A locally refused reaction never left the plugin: report the
|
|
1721
|
+
// refusal so the model does not tell the user it reacted.
|
|
1722
|
+
if (!sent.sent) return reactionBlockedError(chatId, sent.reason);
|
|
1416
1723
|
return jsonResponse({ reacted: true, targetMessageId, emoji, removed });
|
|
1417
1724
|
} catch (err) {
|
|
1418
1725
|
return genericError(err);
|
|
@@ -2023,7 +2330,7 @@ export function registerOpenclawClawlingTools(
|
|
|
2023
2330
|
async execute(_callId, params) {
|
|
2024
2331
|
return await recordClawchatToolCall(accountId, "clawchat_orchestrate_get_group", params, async () => {
|
|
2025
2332
|
const p = params as ClawchatOrchGetGroupParams;
|
|
2026
|
-
return await withClient(accountId, (c) => c.orchGetGroup(p.conversationId));
|
|
2333
|
+
return await withClient(accountId, (c) => c.orchGetGroup(normalizeToolChatId(p.conversationId)));
|
|
2027
2334
|
});
|
|
2028
2335
|
},
|
|
2029
2336
|
};
|
|
@@ -2056,7 +2363,7 @@ export function registerOpenclawClawlingTools(
|
|
|
2056
2363
|
params,
|
|
2057
2364
|
async () => {
|
|
2058
2365
|
const p = params as ClawchatOrchSetGroupPromptParams;
|
|
2059
|
-
return await withClient(accountId, (c) => c.orchSetGroupPrompt(p.conversationId, p.description));
|
|
2366
|
+
return await withClient(accountId, (c) => c.orchSetGroupPrompt(normalizeToolChatId(p.conversationId), p.description));
|
|
2060
2367
|
},
|
|
2061
2368
|
);
|
|
2062
2369
|
},
|
|
@@ -2106,7 +2413,7 @@ export function registerOpenclawClawlingTools(
|
|
|
2106
2413
|
params,
|
|
2107
2414
|
async () => {
|
|
2108
2415
|
const p = params as ClawchatOrchGroupMemberParams;
|
|
2109
|
-
return await withClient(accountId, (c) => c.orchAddGroupMember(p.conversationId, p.agentId));
|
|
2416
|
+
return await withClient(accountId, (c) => c.orchAddGroupMember(normalizeToolChatId(p.conversationId), p.agentId));
|
|
2110
2417
|
},
|
|
2111
2418
|
);
|
|
2112
2419
|
},
|
|
@@ -2133,7 +2440,7 @@ export function registerOpenclawClawlingTools(
|
|
|
2133
2440
|
params,
|
|
2134
2441
|
async () => {
|
|
2135
2442
|
const p = params as ClawchatOrchGroupMemberParams;
|
|
2136
|
-
return await withClient(accountId, (c) => c.orchRemoveGroupMember(p.conversationId, p.agentId));
|
|
2443
|
+
return await withClient(accountId, (c) => c.orchRemoveGroupMember(normalizeToolChatId(p.conversationId), p.agentId));
|
|
2137
2444
|
},
|
|
2138
2445
|
);
|
|
2139
2446
|
},
|
|
@@ -2163,7 +2470,7 @@ export function registerOpenclawClawlingTools(
|
|
|
2163
2470
|
async () => {
|
|
2164
2471
|
const p = params as ClawchatOrchSetGroupAgentSettingsParams;
|
|
2165
2472
|
return await withClient(accountId, (c) =>
|
|
2166
|
-
c.orchSetGroupAgentSettings(p.conversationId, p.agentId, {
|
|
2473
|
+
c.orchSetGroupAgentSettings(normalizeToolChatId(p.conversationId), p.agentId, {
|
|
2167
2474
|
muted: p.muted,
|
|
2168
2475
|
replyMode: p.replyMode,
|
|
2169
2476
|
batchDelaySeconds: p.batchDelaySeconds,
|