@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21

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.
Files changed (99) hide show
  1. package/README.md +39 -17
  2. package/dist/index.js +3 -1
  3. package/dist/src/api-client.js +71 -12
  4. package/dist/src/api-types.test-d.js +10 -0
  5. package/dist/src/channel.js +5 -5
  6. package/dist/src/channel.setup.js +4 -17
  7. package/dist/src/clawchat-memory.js +290 -0
  8. package/dist/src/clawchat-metadata.js +235 -0
  9. package/dist/src/client.js +31 -93
  10. package/dist/src/commands.js +3 -3
  11. package/dist/src/config.js +58 -3
  12. package/dist/src/group-message-coalescer.js +107 -0
  13. package/dist/src/inbound.js +24 -28
  14. package/dist/src/login.runtime.js +82 -19
  15. package/dist/src/media-runtime.js +2 -3
  16. package/dist/src/message-mapper.js +1 -1
  17. package/dist/src/mock-transport.js +31 -0
  18. package/dist/src/outbound.js +281 -56
  19. package/dist/src/plugin-prompts.js +76 -0
  20. package/dist/src/profile-prompt.js +150 -0
  21. package/dist/src/profile-sync.js +169 -0
  22. package/dist/src/prompt-injection.js +25 -0
  23. package/dist/src/protocol-types.js +63 -0
  24. package/dist/src/protocol-types.typecheck.js +1 -0
  25. package/dist/src/protocol.js +2 -2
  26. package/dist/src/reply-dispatcher.js +143 -40
  27. package/dist/src/runtime.js +813 -109
  28. package/dist/src/storage.js +636 -0
  29. package/dist/src/tools-schema.js +70 -10
  30. package/dist/src/tools.js +600 -112
  31. package/dist/src/ws-alignment.js +8 -0
  32. package/dist/src/ws-client.js +588 -0
  33. package/index.ts +6 -1
  34. package/openclaw.plugin.json +44 -4
  35. package/package.json +4 -3
  36. package/prompts/platform.md +7 -0
  37. package/skills/clawchat/SKILL.md +90 -0
  38. package/src/api-client.test.ts +360 -15
  39. package/src/api-client.ts +127 -25
  40. package/src/api-types.test-d.ts +12 -0
  41. package/src/api-types.ts +71 -4
  42. package/src/buffered-stream.test.ts +1 -1
  43. package/src/buffered-stream.ts +1 -1
  44. package/src/channel.outbound.test.ts +270 -60
  45. package/src/channel.setup.ts +9 -18
  46. package/src/channel.test.ts +33 -25
  47. package/src/channel.ts +5 -7
  48. package/src/clawchat-memory.test.ts +372 -0
  49. package/src/clawchat-memory.ts +363 -0
  50. package/src/clawchat-metadata.test.ts +350 -0
  51. package/src/clawchat-metadata.ts +352 -0
  52. package/src/client.test.ts +57 -48
  53. package/src/client.ts +37 -129
  54. package/src/commands.test.ts +2 -2
  55. package/src/commands.ts +3 -3
  56. package/src/config.test.ts +169 -4
  57. package/src/config.ts +86 -6
  58. package/src/group-message-coalescer.test.ts +223 -0
  59. package/src/group-message-coalescer.ts +154 -0
  60. package/src/inbound.test.ts +106 -19
  61. package/src/inbound.ts +31 -35
  62. package/src/login.runtime.test.ts +294 -11
  63. package/src/login.runtime.ts +90 -21
  64. package/src/manifest.test.ts +86 -14
  65. package/src/media-runtime.test.ts +31 -2
  66. package/src/media-runtime.ts +7 -10
  67. package/src/message-mapper.test.ts +2 -2
  68. package/src/message-mapper.ts +2 -2
  69. package/src/mock-transport.test.ts +35 -0
  70. package/src/mock-transport.ts +38 -0
  71. package/src/outbound.test.ts +811 -95
  72. package/src/outbound.ts +332 -65
  73. package/src/plugin-entry.test.ts +3 -1
  74. package/src/plugin-prompts.test.ts +78 -0
  75. package/src/plugin-prompts.ts +92 -0
  76. package/src/profile-prompt.test.ts +435 -0
  77. package/src/profile-prompt.ts +208 -0
  78. package/src/profile-sync.test.ts +611 -0
  79. package/src/profile-sync.ts +268 -0
  80. package/src/prompt-injection.test.ts +39 -0
  81. package/src/prompt-injection.ts +45 -0
  82. package/src/protocol-types.test.ts +69 -0
  83. package/src/protocol-types.ts +296 -0
  84. package/src/protocol-types.typecheck.ts +89 -0
  85. package/src/protocol.ts +2 -2
  86. package/src/reply-dispatcher.test.ts +720 -135
  87. package/src/reply-dispatcher.ts +174 -42
  88. package/src/runtime.test.ts +3884 -337
  89. package/src/runtime.ts +956 -128
  90. package/src/storage.test.ts +692 -0
  91. package/src/storage.ts +989 -0
  92. package/src/streaming.test.ts +1 -1
  93. package/src/streaming.ts +1 -1
  94. package/src/tools-schema.ts +115 -13
  95. package/src/tools.test.ts +501 -10
  96. package/src/tools.ts +739 -133
  97. package/src/ws-alignment.ts +9 -0
  98. package/src/ws-client.test.ts +1218 -0
  99. package/src/ws-client.ts +662 -0
package/src/tools.test.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  import fs from "node:fs";
2
- import { describe, expect, it, vi } from "vitest";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { Value } from "@sinclair/typebox/value";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
6
  import { registerOpenclawClawlingTools } from "./tools.ts";
7
+ import * as toolSchemas from "./tools-schema.ts";
8
+
9
+ const schemas = toolSchemas as Record<string, unknown>;
4
10
 
5
11
  interface RegisteredTool {
6
12
  name: string;
13
+ description?: string;
14
+ parameters?: unknown;
7
15
  execute: (callId: string, params: unknown) => Promise<unknown>;
8
16
  }
9
17
 
@@ -13,9 +21,16 @@ const ALWAYS_VISIBLE_TOOL_NAMES = [
13
21
  "clawchat_delete_moment",
14
22
  "clawchat_delete_moment_comment",
15
23
  "clawchat_get_account_profile",
24
+ "clawchat_get_conversation",
16
25
  "clawchat_get_user_profile",
17
26
  "clawchat_list_account_friends",
27
+ "clawchat_list_conversations",
18
28
  "clawchat_list_moments",
29
+ "clawchat_memory_edit",
30
+ "clawchat_memory_read",
31
+ "clawchat_memory_write",
32
+ "clawchat_metadata_sync",
33
+ "clawchat_metadata_update",
19
34
  "clawchat_reply_moment_comment",
20
35
  "clawchat_search_users",
21
36
  "clawchat_toggle_moment_reaction",
@@ -24,12 +39,26 @@ const ALWAYS_VISIBLE_TOOL_NAMES = [
24
39
  "clawchat_upload_media_file",
25
40
  ];
26
41
 
42
+ const tempRoots: string[] = [];
43
+
44
+ afterEach(() => {
45
+ for (const dir of tempRoots.splice(0)) {
46
+ fs.rmSync(dir, { recursive: true, force: true });
47
+ }
48
+ });
49
+
27
50
  function buildApi(opts: {
28
51
  configChannel?: Record<string, unknown> | null;
29
52
  configTools?: Record<string, unknown>;
53
+ workspaceDir?: string;
30
54
  registerTool?: (tool: { name: string }, options?: { name: string }) => void;
31
55
  }) {
32
56
  const registered: RegisteredTool[] = [];
57
+ const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-tools-"));
58
+ const workspaceDir =
59
+ opts.workspaceDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-workspace-"));
60
+ tempRoots.push(stateDir);
61
+ tempRoots.push(workspaceDir);
33
62
  const api = {
34
63
  config:
35
64
  opts.configChannel === null
@@ -45,12 +74,32 @@ function buildApi(opts: {
45
74
  error: vi.fn(),
46
75
  },
47
76
  runtime: {
77
+ state: {
78
+ resolveStateDir: () => stateDir,
79
+ },
48
80
  config: {
49
81
  mutateConfigFile: vi.fn(),
50
82
  },
51
83
  },
52
- registerTool: (tool: RegisteredTool, _options?: { name: string }) => {
53
- registered.push(tool);
84
+ registerTool: (tool: RegisteredTool | ((ctx: unknown) => RegisteredTool | RegisteredTool[] | null | undefined), _options?: { name: string }) => {
85
+ const resolved =
86
+ typeof tool === "function"
87
+ ? tool({
88
+ workspaceDir,
89
+ agentId: "agent-default",
90
+ runtimeConfig: {
91
+ channels: { "openclaw-clawchat": opts.configChannel ?? {} },
92
+ },
93
+ getRuntimeConfig: () => ({
94
+ channels: { "openclaw-clawchat": opts.configChannel ?? {} },
95
+ }),
96
+ })
97
+ : tool;
98
+ if (Array.isArray(resolved)) {
99
+ registered.push(...resolved);
100
+ } else if (resolved) {
101
+ registered.push(resolved);
102
+ }
54
103
  },
55
104
  } as unknown as Parameters<typeof registerOpenclawClawlingTools>[0];
56
105
  return { api, registered };
@@ -60,6 +109,7 @@ function configuredChannel(extra: Record<string, unknown> = {}) {
60
109
  return {
61
110
  websocketUrl: "wss://w",
62
111
  token: "tk",
112
+ agentId: "agt-1",
63
113
  userId: "u",
64
114
  ...extra,
65
115
  };
@@ -105,6 +155,25 @@ describe("registerOpenclawClawlingTools", () => {
105
155
  expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
106
156
  });
107
157
 
158
+ it("registers only read-only conversation tools", () => {
159
+ const { api, registered } = buildApi({
160
+ configChannel: configuredChannel(),
161
+ });
162
+
163
+ registerOpenclawClawlingTools(api);
164
+
165
+ const names = registered.map((t) => t.name);
166
+ expect(names).toContain("clawchat_list_conversations");
167
+ expect(names).toContain("clawchat_get_conversation");
168
+ expect(names).not.toContain("clawchat_create_group_conversation");
169
+ expect(names).not.toContain("clawchat_update_conversation");
170
+ expect(names).not.toContain("clawchat_leave_conversation");
171
+ expect(names).not.toContain("clawchat_dissolve_conversation");
172
+ expect(names).not.toContain("clawchat_add_conversation_member");
173
+ expect(names).not.toContain("clawchat_remove_conversation_member");
174
+ expect(names).not.toContain("clawchat_list_conversation_users");
175
+ });
176
+
108
177
  it("skips registration when api.config is undefined", () => {
109
178
  const { api, registered } = buildApi({ configChannel: null });
110
179
  registerOpenclawClawlingTools(api);
@@ -133,10 +202,143 @@ describe("registerOpenclawClawlingTools", () => {
133
202
 
134
203
  expect(logger.info).not.toHaveBeenCalled();
135
204
  expect(logger.debug).toHaveBeenCalledWith(
136
- "openclaw-clawchat: registered 14 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, search_users, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, upload_media_file)",
205
+ "openclaw-clawchat: registered 21 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, search_users, list_conversations, get_conversation, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, upload_media_file, memory_read, memory_write, memory_edit, metadata_sync, metadata_update)",
137
206
  );
138
207
  });
139
208
 
209
+ it("registers exactly the five ClawChat memory and metadata tools without old-prefix aliases", () => {
210
+ const { api, registered } = buildApi({
211
+ configChannel: configuredChannel({ ownerUserId: "owner" }),
212
+ });
213
+
214
+ registerOpenclawClawlingTools(api);
215
+
216
+ const names = registered.map((t) => t.name).sort();
217
+ expect(names.filter((name) => /memory|metadata/.test(name))).toEqual([
218
+ "clawchat_memory_edit",
219
+ "clawchat_memory_read",
220
+ "clawchat_memory_write",
221
+ "clawchat_metadata_sync",
222
+ "clawchat_metadata_update",
223
+ ]);
224
+ expect(names).not.toContain("cc_memory_read");
225
+ expect(names).not.toContain("cc_metadata_sync");
226
+ });
227
+
228
+ it("keeps old ClawChat tools registered and guarded against direct HTTP or shell fallbacks", () => {
229
+ const { api, registered } = buildApi({
230
+ configChannel: configuredChannel({ ownerUserId: "owner" }),
231
+ });
232
+
233
+ registerOpenclawClawlingTools(api);
234
+
235
+ const oldTools = registered.filter((tool) => !/memory|metadata/.test(tool.name));
236
+ expect(oldTools.map((tool) => tool.name).sort()).toEqual(
237
+ ALWAYS_VISIBLE_TOOL_NAMES.filter((name) => !/memory|metadata/.test(name)),
238
+ );
239
+ for (const tool of oldTools) {
240
+ expect(tool.description).toMatch(/Do not use execute/);
241
+ expect(tool.description).toMatch(/shell commands/);
242
+ expect(tool.description).toMatch(/curl/);
243
+ expect(tool.description).toMatch(/direct ClawChat HTTP calls/);
244
+ expect(tool.description).toMatch(/generic fallback tools/);
245
+ }
246
+ });
247
+
248
+ it("memory and metadata schemas require explicit targets and bounded modes", () => {
249
+ const readSchema = schemas.ClawchatMemoryReadSchema;
250
+ const writeSchema = schemas.ClawchatMemoryWriteSchema;
251
+ const editSchema = schemas.ClawchatMemoryEditSchema;
252
+ const syncSchema = schemas.ClawchatMetadataSyncSchema;
253
+ const updateSchema = schemas.ClawchatMetadataUpdateSchema;
254
+ expect(readSchema).toBeDefined();
255
+ expect(writeSchema).toBeDefined();
256
+ expect(editSchema).toBeDefined();
257
+ expect(syncSchema).toBeDefined();
258
+ expect(updateSchema).toBeDefined();
259
+
260
+ for (const schema of [readSchema, writeSchema, editSchema, syncSchema, updateSchema]) {
261
+ expect((schema as { type?: unknown }).type).toBe("object");
262
+ expect(schema).not.toHaveProperty("anyOf");
263
+ expect(schema).not.toHaveProperty("oneOf");
264
+ }
265
+
266
+ for (const targetType of ["owner", "user", "group"] as const) {
267
+ expect(Value.Check(readSchema, { targetType, targetId: targetType === "owner" ? "owner" : "id_1" })).toBe(true);
268
+ }
269
+ expect(Value.Check(readSchema, { targetType: "profile", targetId: "id_1" })).toBe(false);
270
+ expect(Value.Check(readSchema, { targetType: "user" })).toBe(false);
271
+ expect(Value.Check(readSchema, { targetType: "user", targetId: "id_1", filePath: "users/id_1.md" })).toBe(false);
272
+
273
+ expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "append", content: "note" })).toBe(true);
274
+ expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "replace", content: "" })).toBe(true);
275
+ expect(Value.Check(writeSchema, { targetType: "user", targetId: "usr_1", mode: "merge", content: "note" })).toBe(false);
276
+
277
+ expect(Value.Check(syncSchema, { targetType: "owner", targetId: "owner", direction: "pull" })).toBe(true);
278
+ expect(Value.Check(syncSchema, { targetType: "group", targetId: "grp_1", direction: "push" })).toBe(true);
279
+ expect(Value.Check(syncSchema, { targetType: "group", targetId: "grp_1", direction: "refresh" })).toBe(false);
280
+ });
281
+
282
+ it("metadata update schema requires a non-empty whitelisted patch", () => {
283
+ const schema = schemas.ClawchatMetadataUpdateSchema;
284
+ expect(schema).toBeDefined();
285
+
286
+ expect(Value.Check(schema, {
287
+ targetType: "owner",
288
+ targetId: "owner",
289
+ patch: { nickname: "Bot" },
290
+ })).toBe(true);
291
+ expect(Value.Check(schema, {
292
+ targetType: "user",
293
+ targetId: "usr_1",
294
+ patch: { avatar_url: "https://cdn/avatar.png", bio: "" },
295
+ })).toBe(true);
296
+ expect(Value.Check(schema, {
297
+ targetType: "group",
298
+ targetId: "grp_1",
299
+ patch: { title: "Team", description: "Planning" },
300
+ })).toBe(true);
301
+ expect(Value.Check(schema, { targetType: "owner", targetId: "owner" })).toBe(false);
302
+ expect(Value.Check(schema, { targetType: "owner", targetId: "owner", patch: {} })).toBe(false);
303
+ for (const key of ["visibility", "status", "platform", "raw", "kind"]) {
304
+ expect(Value.Check(schema, {
305
+ targetType: "owner",
306
+ targetId: "owner",
307
+ patch: { [key]: "not allowed" },
308
+ })).toBe(false);
309
+ expect(Value.Check(schema, {
310
+ targetType: "group",
311
+ targetId: "grp_1",
312
+ patch: { [key]: "not allowed" },
313
+ })).toBe(false);
314
+ }
315
+ });
316
+
317
+ it("memory tool safety errors do not expose workspace paths", async () => {
318
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-workspace-"));
319
+ tempRoots.push(workspaceDir);
320
+ fs.symlinkSync(os.tmpdir(), path.join(workspaceDir, "users"));
321
+ const { api, registered } = buildApi({
322
+ configChannel: configuredChannel({ ownerUserId: "owner" }),
323
+ workspaceDir,
324
+ });
325
+
326
+ registerOpenclawClawlingTools(api);
327
+ const tool = registered.find((t) => t.name === "clawchat_memory_read")!;
328
+ const result = await tool.execute("call-1", { targetType: "user", targetId: "usr_1" });
329
+
330
+ const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
331
+ error?: string;
332
+ message?: string;
333
+ };
334
+ expect(parsed).toMatchObject({
335
+ error: "validation",
336
+ message: "openclaw-clawchat: unsafe ClawChat memory target",
337
+ });
338
+ expect(parsed.message).not.toContain(workspaceDir);
339
+ expect(parsed.message).not.toContain(os.tmpdir());
340
+ });
341
+
140
342
  it("account tools return a config error before activation instead of disappearing", async () => {
141
343
  const { api, registered } = buildApi({
142
344
  configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
@@ -152,6 +354,285 @@ describe("registerOpenclawClawlingTools", () => {
152
354
  expect(parsed.message).toMatch(/token is required/i);
153
355
  });
154
356
 
357
+ it("records successful clawchat tool calls without changing the returned result", async () => {
358
+ const store = { recordToolCall: vi.fn() };
359
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
360
+ new Response(
361
+ JSON.stringify({
362
+ code: 0,
363
+ msg: "ok",
364
+ data: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
365
+ }),
366
+ { status: 200, headers: { "content-type": "application/json" } },
367
+ ),
368
+ );
369
+ try {
370
+ const { api, registered } = buildApi({
371
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
372
+ });
373
+ registerOpenclawClawlingTools(api, { store });
374
+ const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
375
+
376
+ const result = await tool.execute("call-1", {});
377
+
378
+ expect((result as { details: unknown }).details).toEqual({
379
+ user_id: "u",
380
+ nickname: "Bot",
381
+ avatar_url: "",
382
+ bio: "",
383
+ });
384
+ expect(store.recordToolCall).toHaveBeenCalledTimes(1);
385
+ expect(store.recordToolCall).toHaveBeenCalledWith(
386
+ expect.objectContaining({
387
+ platform: "openclaw",
388
+ accountId: "default",
389
+ toolName: "clawchat_get_account_profile",
390
+ args: {},
391
+ result: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
392
+ error: null,
393
+ startedAt: expect.any(Number),
394
+ endedAt: expect.any(Number),
395
+ }),
396
+ );
397
+ } finally {
398
+ fetchMock.mockRestore();
399
+ }
400
+ });
401
+
402
+ it("lists conversations, upserts returned summaries, and does not fetch or delete extra cache entries", async () => {
403
+ const store = {
404
+ recordToolCall: vi.fn(),
405
+ upsertConversationSummary: vi.fn(),
406
+ upsertConversationDetails: vi.fn(),
407
+ deleteConversationCache: vi.fn(),
408
+ };
409
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
410
+ new Response(
411
+ JSON.stringify({
412
+ code: 0,
413
+ msg: "ok",
414
+ data: {
415
+ conversations: [
416
+ {
417
+ id: "cnv_1",
418
+ type: "direct",
419
+ title: "Alice",
420
+ created_at: "2026-05-01T00:00:00Z",
421
+ updated_at: "2026-05-02T00:00:00Z",
422
+ peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
423
+ },
424
+ ],
425
+ next_before: "cursor-2",
426
+ },
427
+ }),
428
+ { status: 200, headers: { "content-type": "application/json" } },
429
+ ),
430
+ );
431
+ try {
432
+ const { api, registered } = buildApi({
433
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
434
+ });
435
+ registerOpenclawClawlingTools(api, { store });
436
+ const tool = registered.find((t) => t.name === "clawchat_list_conversations")!;
437
+
438
+ const result = await tool.execute("call-1", { before: "cursor-1", limit: 10 });
439
+
440
+ expect((result as { details: unknown }).details).toEqual({
441
+ conversations: [
442
+ {
443
+ id: "cnv_1",
444
+ type: "direct",
445
+ title: "Alice",
446
+ created_at: "2026-05-01T00:00:00Z",
447
+ updated_at: "2026-05-02T00:00:00Z",
448
+ peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
449
+ },
450
+ ],
451
+ next_before: "cursor-2",
452
+ });
453
+ expect(fetchMock).toHaveBeenCalledTimes(1);
454
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations?before=cursor-1&limit=10");
455
+ expect(store.upsertConversationSummary).toHaveBeenCalledWith(
456
+ expect.objectContaining({
457
+ platform: "openclaw",
458
+ accountId: "default",
459
+ conversationId: "cnv_1",
460
+ conversationType: "direct",
461
+ raw: expect.objectContaining({ id: "cnv_1" }),
462
+ }),
463
+ );
464
+ expect(store.upsertConversationDetails).not.toHaveBeenCalled();
465
+ expect(store.deleteConversationCache).not.toHaveBeenCalled();
466
+ } finally {
467
+ fetchMock.mockRestore();
468
+ }
469
+ });
470
+
471
+ it("gets a conversation and upserts full details", async () => {
472
+ const store = {
473
+ recordToolCall: vi.fn(),
474
+ upsertConversationSummary: vi.fn(),
475
+ upsertConversationDetails: vi.fn(),
476
+ deleteConversationCache: vi.fn(),
477
+ };
478
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
479
+ new Response(
480
+ JSON.stringify({
481
+ code: 0,
482
+ msg: "ok",
483
+ data: {
484
+ conversation: {
485
+ id: "group_1",
486
+ type: "group",
487
+ title: "Project Group",
488
+ creator_id: "owner",
489
+ created_at: "2026-05-01T00:00:00Z",
490
+ updated_at: "2026-05-02T00:00:00Z",
491
+ participants: [
492
+ { conversation_id: "group_1", user_id: "owner", role: "owner", joined_at: "2026-05-01T00:00:00Z" },
493
+ ],
494
+ },
495
+ },
496
+ }),
497
+ { status: 200, headers: { "content-type": "application/json" } },
498
+ ),
499
+ );
500
+ try {
501
+ const { api, registered } = buildApi({
502
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
503
+ });
504
+ registerOpenclawClawlingTools(api, { store });
505
+ const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
506
+
507
+ await tool.execute("call-1", { conversationId: "group_1" });
508
+
509
+ expect(fetchMock).toHaveBeenCalledTimes(1);
510
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations/group_1");
511
+ expect(store.upsertConversationDetails).toHaveBeenCalledWith(
512
+ expect.objectContaining({
513
+ platform: "openclaw",
514
+ accountId: "default",
515
+ conversationId: "group_1",
516
+ conversationType: "group",
517
+ raw: expect.objectContaining({ id: "group_1" }),
518
+ members: [expect.objectContaining({ userId: "owner", role: "owner" })],
519
+ membersComplete: true,
520
+ }),
521
+ );
522
+ const details = store.upsertConversationDetails.mock.calls[0]?.[0] as Record<string, unknown>;
523
+ expect(Object.prototype.hasOwnProperty.call(details, "groupProfile")).toBe(false);
524
+ expect(Object.prototype.hasOwnProperty.call(details, "userProfiles")).toBe(false);
525
+ } finally {
526
+ fetchMock.mockRestore();
527
+ }
528
+ });
529
+
530
+ it("deletes conversation-scoped cache and returns the standard error result when get conversation is not found", async () => {
531
+ const store = {
532
+ recordToolCall: vi.fn(),
533
+ upsertConversationSummary: vi.fn(),
534
+ upsertConversationDetails: vi.fn(),
535
+ deleteConversationCache: vi.fn(),
536
+ };
537
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
538
+ new Response(JSON.stringify({ code: 40401, msg: "conversation not found", data: null }), {
539
+ status: 200,
540
+ headers: { "content-type": "application/json" },
541
+ }),
542
+ );
543
+ try {
544
+ const { api, registered } = buildApi({
545
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
546
+ });
547
+ registerOpenclawClawlingTools(api, { store });
548
+ const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
549
+
550
+ const result = await tool.execute("call-1", { conversationId: "missing" });
551
+
552
+ const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
553
+ error?: string;
554
+ message?: string;
555
+ meta?: { code?: number };
556
+ };
557
+ expect(parsed).toMatchObject({
558
+ error: "api",
559
+ message: "conversation not found",
560
+ meta: { code: 40401 },
561
+ });
562
+ expect(store.deleteConversationCache).toHaveBeenCalledWith({
563
+ platform: "openclaw",
564
+ accountId: "default",
565
+ conversationId: "missing",
566
+ });
567
+ expect(store.upsertConversationSummary).not.toHaveBeenCalled();
568
+ expect(store.upsertConversationDetails).not.toHaveBeenCalled();
569
+ } finally {
570
+ fetchMock.mockRestore();
571
+ }
572
+ });
573
+
574
+ it("does not write conversation cache from account, user, friend, search, avatar, or media tools", async () => {
575
+ const store = {
576
+ recordToolCall: vi.fn(),
577
+ upsertConversationSummary: vi.fn(),
578
+ upsertConversationDetails: vi.fn(),
579
+ deleteConversationCache: vi.fn(),
580
+ };
581
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
582
+ new Response(
583
+ JSON.stringify({
584
+ code: 0,
585
+ msg: "ok",
586
+ data: { id: "u", nickname: "Bot", avatar_url: "", bio: "" },
587
+ }),
588
+ { status: 200, headers: { "content-type": "application/json" } },
589
+ ),
590
+ );
591
+ try {
592
+ const { api, registered } = buildApi({
593
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
594
+ });
595
+ registerOpenclawClawlingTools(api, { store });
596
+ const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
597
+
598
+ await tool.execute("call-1", {});
599
+
600
+ expect(store.upsertConversationSummary).not.toHaveBeenCalled();
601
+ expect(store.upsertConversationDetails).not.toHaveBeenCalled();
602
+ expect(store.deleteConversationCache).not.toHaveBeenCalled();
603
+ } finally {
604
+ fetchMock.mockRestore();
605
+ }
606
+ });
607
+
608
+ it("records clawchat tool failures while preserving the returned error result", async () => {
609
+ const store = { recordToolCall: vi.fn() };
610
+ const { api, registered } = buildApi({
611
+ configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
612
+ });
613
+ registerOpenclawClawlingTools(api, { store });
614
+ const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
615
+
616
+ const result = await tool.execute("call-1", {});
617
+
618
+ const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
619
+ error?: string;
620
+ message?: string;
621
+ };
622
+ expect(parsed).toMatchObject({ error: "config" });
623
+ expect(store.recordToolCall).toHaveBeenCalledWith(
624
+ expect.objectContaining({
625
+ platform: "openclaw",
626
+ accountId: "default",
627
+ toolName: "clawchat_get_account_profile",
628
+ args: {},
629
+ result: parsed,
630
+ error: expect.stringContaining("config"),
631
+ }),
632
+ );
633
+ expect(registered.every((tool) => tool.name.startsWith("clawchat_"))).toBe(true);
634
+ });
635
+
155
636
  it("clawchat_update_account_profile description names account profile triggers (EN + ZH)", () => {
156
637
  const { api } = buildApi({ configChannel: configuredChannel() });
157
638
  const fullTools: Array<{ name: string; description?: string }> = [];
@@ -175,15 +656,13 @@ describe("registerOpenclawClawlingTools", () => {
175
656
  });
176
657
 
177
658
  it("account query and upload tool descriptions include precise trigger semantics", () => {
178
- const { api } = buildApi({ configChannel: configuredChannel() });
179
- const fullTools: Array<{ name: string; description?: string }> = [];
180
- api.registerTool = (tool: { name: string; description?: string }) => {
181
- fullTools.push(tool);
182
- };
659
+ const { api, registered: fullTools } = buildApi({ configChannel: configuredChannel() });
183
660
  registerOpenclawClawlingTools(api);
184
661
 
185
662
  const accountProfile = fullTools.find((t) => t.name === "clawchat_get_account_profile")!;
186
663
  const userProfile = fullTools.find((t) => t.name === "clawchat_get_user_profile")!;
664
+ const memoryWrite = fullTools.find((t) => t.name === "clawchat_memory_write")!;
665
+ const metadataSync = fullTools.find((t) => t.name === "clawchat_metadata_sync")!;
187
666
  const friends = fullTools.find((t) => t.name === "clawchat_list_account_friends")!;
188
667
  const avatar = fullTools.find((t) => t.name === "clawchat_upload_avatar_image")!;
189
668
  const media = fullTools.find((t) => t.name === "clawchat_upload_media_file")!;
@@ -195,10 +674,22 @@ describe("registerOpenclawClawlingTools", () => {
195
674
  expect(userProfile.description).toMatch(/userId/);
196
675
  expect(userProfile.description).toMatch(/public profile/i);
197
676
  expect(userProfile.description).toMatch(/do not guess|do not infer/i);
677
+ expect(userProfile.description).toMatch(/read-only lookup/i);
678
+ expect(userProfile.description).toMatch(/clawchat_metadata_sync.*direction=pull/i);
679
+
680
+ expect(memoryWrite.description).toMatch(/agent-authored body/i);
681
+ expect(memoryWrite.description).toMatch(/never modifies the metadata block/i);
682
+ expect(memoryWrite.description).toMatch(/Do not use.*nickname.*avatar_url.*bio.*profile_type/i);
683
+ expect(memoryWrite.description).toMatch(/refresh.*local ClawChat.*profile.*clawchat_metadata_sync.*direction=pull/i);
684
+
685
+ expect(metadataSync.description).toMatch(/refresh, sync, or update local ClawChat profile information/i);
686
+ expect(metadataSync.description).toMatch(/rewrites only the metadata block/i);
687
+ expect(metadataSync.description).toMatch(/Do not combine clawchat_get_user_profile with clawchat_memory_write/i);
198
688
 
199
689
  expect(friends.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
200
690
  expect(friends.description).toMatch(/friends|contacts/i);
201
- expect(friends.description).toMatch(/page=1|pageSize=20/);
691
+ expect(friends.description).not.toMatch(/paginated|page=1|pageSize=20/i);
692
+ expect(friends.parameters?.properties ?? {}).toEqual({});
202
693
 
203
694
  expect(avatar.description).toMatch(/local image/i);
204
695
  expect(avatar.description).toMatch(/avatar URL|hosted avatar URL|public URL/i);