@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
@@ -0,0 +1,692 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { createClawChatStore, getClawChatStore, resetClawChatStoreForTest } from "./storage.ts";
7
+
8
+ const tempRoots: string[] = [];
9
+
10
+ function tempDbPath(): string {
11
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-storage-"));
12
+ tempRoots.push(dir);
13
+ return path.join(dir, "clawchat.sqlite");
14
+ }
15
+
16
+ function readSqliteRows(dbPath: string, sql: string): Record<string, unknown>[] {
17
+ const db = new DatabaseSync(dbPath, { readOnly: true });
18
+ try {
19
+ return db.prepare(sql).all() as Record<string, unknown>[];
20
+ } finally {
21
+ db.close();
22
+ }
23
+ }
24
+
25
+ afterEach(() => {
26
+ vi.useRealTimers();
27
+ resetClawChatStoreForTest();
28
+ for (const dir of tempRoots.splice(0)) {
29
+ fs.rmSync(dir, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ describe("clawchat sqlite storage", () => {
34
+ it("migrates connection metadata without creating legacy profile cache tables", () => {
35
+ const dbPath = tempDbPath();
36
+ const store = createClawChatStore({ dbPath });
37
+
38
+ store.initialize();
39
+ store.close();
40
+
41
+ expect(readSqliteRows(dbPath, "PRAGMA table_info(connections)").map((row) => row.name)).toEqual(
42
+ expect.arrayContaining(["resolved_device_id", "delivery_mode"]),
43
+ );
44
+ expect(
45
+ readSqliteRows(
46
+ dbPath,
47
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'clawchat_%' ORDER BY name",
48
+ ).map((row) => row.name),
49
+ ).toEqual(
50
+ expect.arrayContaining([
51
+ "clawchat_conversation_members",
52
+ "clawchat_conversations",
53
+ ]),
54
+ );
55
+ expect(
56
+ readSqliteRows(
57
+ dbPath,
58
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('clawchat_profiles', 'clawchat_user_profiles', 'clawchat_group_profiles') ORDER BY name",
59
+ ),
60
+ ).toEqual([]);
61
+ });
62
+
63
+ it("does not expose legacy profile helper methods", () => {
64
+ const store = createClawChatStore({ dbPath: tempDbPath() });
65
+
66
+ expect("upsertProfile" in store).toBe(false);
67
+ expect("upsertMinimalProfile" in store).toBe(false);
68
+ expect("upsertAgentProfile" in store).toBe(false);
69
+ expect("upsertGroupProfile" in store).toBe(false);
70
+ expect("upsertUserProfile" in store).toBe(false);
71
+ expect("profileExists" in store).toBe(false);
72
+ expect("getProfile" in store).toBe(false);
73
+
74
+ store.close();
75
+ });
76
+
77
+ it("initializes schema and records activation/message/connection/tool rows", () => {
78
+ const store = createClawChatStore({ dbPath: tempDbPath() });
79
+
80
+ expect(store.listAppliedMigrations()).toEqual([
81
+ { version: 1, name: "initial_schema" },
82
+ { version: 2, name: "message_idempotency" },
83
+ { version: 3, name: "activation_bootstrap" },
84
+ { version: 4, name: "activation_owner_user_id" },
85
+ { version: 5, name: "conversation_cache" },
86
+ ]);
87
+
88
+ store.upsertActivation({
89
+ platform: "openclaw",
90
+ accountId: "default",
91
+ userId: "user-old",
92
+ ownerUserId: "owner-old",
93
+ accessToken: "token-old",
94
+ refreshToken: "refresh-old",
95
+ activatedAt: 1000,
96
+ loginMethod: "login",
97
+ });
98
+ store.upsertActivation({
99
+ platform: "openclaw",
100
+ accountId: "default",
101
+ userId: " user-new ",
102
+ ownerUserId: " owner-new ",
103
+ accessToken: "token-new",
104
+ refreshToken: null,
105
+ activatedAt: 2000,
106
+ loginMethod: "login",
107
+ });
108
+
109
+ expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
110
+ user_id: "user-new",
111
+ owner_user_id: "owner-new",
112
+ access_token: null,
113
+ refresh_token: null,
114
+ activated_at: 2000,
115
+ updated_at: 2000,
116
+ });
117
+
118
+ store.insertMessage({
119
+ platform: "openclaw",
120
+ accountId: "default",
121
+ kind: "message",
122
+ direction: "outbound",
123
+ eventType: "message.send",
124
+ traceId: "trace-1",
125
+ chatId: "chat-1",
126
+ messageId: "msg-1",
127
+ text: "hello",
128
+ raw: { ok: true },
129
+ createdAt: 3000,
130
+ });
131
+
132
+ expect(store.listMessagesForTest()).toMatchObject([
133
+ { kind: "message", direction: "outbound", message_id: "msg-1", text: "hello" },
134
+ ]);
135
+
136
+ const connectionId = store.startConnection({
137
+ platform: "openclaw",
138
+ accountId: "default",
139
+ attempt: 1,
140
+ reconnectCount: 0,
141
+ connectStartedAt: 1000,
142
+ });
143
+ expect(connectionId).toEqual(expect.any(Number));
144
+ store.markConnectSent(connectionId, { at: 1100 });
145
+ store.markConnectionReady(connectionId, {
146
+ at: 1200,
147
+ resolvedDeviceId: "device-resolved",
148
+ deliveryMode: "realtime",
149
+ });
150
+ store.finishConnection(connectionId, { state: "disconnected", disconnectedAt: 1300 });
151
+
152
+ expect(store.listConnectionsForTest()).toMatchObject([
153
+ {
154
+ state: "disconnected",
155
+ connect_started_at: 1000,
156
+ connect_sent_at: 1100,
157
+ ready_at: 1200,
158
+ disconnected_at: 1300,
159
+ resolved_device_id: "device-resolved",
160
+ delivery_mode: "realtime",
161
+ },
162
+ ]);
163
+
164
+ store.recordToolCall({
165
+ platform: "openclaw",
166
+ accountId: "default",
167
+ toolName: "clawchat_get_account_profile",
168
+ args: { include: "profile" },
169
+ result: { id: "user-new" },
170
+ error: null,
171
+ startedAt: 4000,
172
+ endedAt: 4250,
173
+ });
174
+
175
+ expect(store.listToolCallsForTest()).toMatchObject([
176
+ { tool_name: "clawchat_get_account_profile", duration_ms: 250, error: null },
177
+ ]);
178
+
179
+ store.close();
180
+ });
181
+
182
+ it("preserves connection ready metadata when omitted on later ready updates", () => {
183
+ const store = createClawChatStore({ dbPath: tempDbPath() });
184
+ const connectionId = store.startConnection({
185
+ platform: "openclaw",
186
+ accountId: "default",
187
+ connectStartedAt: 1000,
188
+ });
189
+
190
+ store.markConnectionReady(connectionId, {
191
+ at: 1100,
192
+ resolvedDeviceId: "device-one",
193
+ deliveryMode: "realtime",
194
+ });
195
+ store.markConnectionReady(connectionId, { at: 1200 });
196
+
197
+ expect(store.listConnectionsForTest()).toMatchObject([
198
+ {
199
+ ready_at: 1200,
200
+ resolved_device_id: "device-one",
201
+ delivery_mode: "realtime",
202
+ },
203
+ ]);
204
+
205
+ store.close();
206
+ });
207
+
208
+ it("atomically claims actual messages by account direction and message_id", () => {
209
+ const store = createClawChatStore({ dbPath: tempDbPath() });
210
+
211
+ expect(store.listAppliedMigrations()).toEqual([
212
+ { version: 1, name: "initial_schema" },
213
+ { version: 2, name: "message_idempotency" },
214
+ { version: 3, name: "activation_bootstrap" },
215
+ { version: 4, name: "activation_owner_user_id" },
216
+ { version: 5, name: "conversation_cache" },
217
+ ]);
218
+
219
+ const first = store.claimMessageOnce({
220
+ platform: "openclaw",
221
+ accountId: "default",
222
+ kind: "message",
223
+ direction: "inbound",
224
+ eventType: "message.send",
225
+ traceId: "trace-1",
226
+ chatId: "chat-1",
227
+ messageId: "msg-unique",
228
+ text: "first",
229
+ raw: { ok: true },
230
+ createdAt: 3000,
231
+ });
232
+ const duplicate = store.claimMessageOnce({
233
+ platform: "openclaw",
234
+ accountId: "default",
235
+ kind: "message",
236
+ direction: "inbound",
237
+ eventType: "message.send",
238
+ traceId: "trace-2",
239
+ chatId: "chat-1",
240
+ messageId: "msg-unique",
241
+ text: "duplicate",
242
+ raw: { ok: true },
243
+ createdAt: 4000,
244
+ });
245
+ const outboundSameId = store.claimMessageOnce({
246
+ platform: "openclaw",
247
+ accountId: "default",
248
+ kind: "message",
249
+ direction: "outbound",
250
+ eventType: "message.reply",
251
+ traceId: "trace-outbound",
252
+ chatId: "chat-1",
253
+ messageId: "msg-unique",
254
+ text: "outbound",
255
+ raw: { ok: true },
256
+ createdAt: 4500,
257
+ });
258
+ const thinking = store.insertMessage({
259
+ platform: "openclaw",
260
+ accountId: "default",
261
+ kind: "thinking",
262
+ direction: "outbound",
263
+ eventType: "message.send",
264
+ traceId: "trace-thinking",
265
+ chatId: "chat-1",
266
+ messageId: "msg-unique",
267
+ text: "reasoning",
268
+ raw: { ok: true },
269
+ createdAt: 5000,
270
+ });
271
+
272
+ expect(first).toBe(true);
273
+ expect(duplicate).toBe(false);
274
+ expect(outboundSameId).toBe(true);
275
+ expect(thinking).toBe(true);
276
+ expect(store.listMessagesForTest()).toMatchObject([
277
+ { kind: "message", direction: "inbound", message_id: "msg-unique", text: "first" },
278
+ { kind: "message", direction: "outbound", message_id: "msg-unique", text: "outbound" },
279
+ { kind: "thinking", message_id: "msg-unique", text: "reasoning" },
280
+ ]);
281
+
282
+ store.close();
283
+ });
284
+
285
+ it("recreates the singleton when a later caller provides a different database path", () => {
286
+ const firstPath = tempDbPath();
287
+ const secondPath = tempDbPath();
288
+
289
+ const first = getClawChatStore({ dbPath: firstPath });
290
+ const second = getClawChatStore({ dbPath: secondPath });
291
+
292
+ expect(second.dbPath).toBe(secondPath);
293
+ expect(second).not.toBe(first);
294
+ });
295
+
296
+ it("stores pending activation bootstrap conversation and marks it sent conditionally", () => {
297
+ const store = createClawChatStore({ dbPath: tempDbPath() });
298
+ const bootstrapStore = store as typeof store & {
299
+ claimPendingActivationBootstrap(input: {
300
+ platform: string;
301
+ accountId: string;
302
+ }): { conversationId: string } | null;
303
+ markActivationBootstrapSent(input: {
304
+ platform: string;
305
+ accountId: string;
306
+ conversationId: string;
307
+ }): boolean | null;
308
+ releaseActivationBootstrapClaim(input: {
309
+ platform: string;
310
+ accountId: string;
311
+ conversationId: string;
312
+ }): boolean | null;
313
+ };
314
+
315
+ store.upsertActivation({
316
+ platform: "openclaw",
317
+ accountId: "default",
318
+ userId: "user-old",
319
+ accessToken: "token-old",
320
+ refreshToken: "refresh-old",
321
+ conversationId: "conv-old",
322
+ activatedAt: 1000,
323
+ loginMethod: "login",
324
+ });
325
+
326
+ expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
327
+ conversation_id: "conv-old",
328
+ bootstrap_sent: 0,
329
+ });
330
+ expect(store.getActivationConversation({ platform: "openclaw", accountId: "default" })).toMatchObject({
331
+ conversationId: "conv-old",
332
+ conversationType: null,
333
+ });
334
+ expect(
335
+ bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
336
+ ).toEqual({
337
+ conversationId: "conv-old",
338
+ });
339
+ expect(
340
+ bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
341
+ ).toBeNull();
342
+ expect(
343
+ bootstrapStore.releaseActivationBootstrapClaim({
344
+ platform: "openclaw",
345
+ accountId: "default",
346
+ conversationId: "conv-other",
347
+ }),
348
+ ).toBe(false);
349
+ expect(
350
+ bootstrapStore.releaseActivationBootstrapClaim({
351
+ platform: "openclaw",
352
+ accountId: "default",
353
+ conversationId: "conv-old",
354
+ }),
355
+ ).toBe(true);
356
+ expect(
357
+ bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
358
+ ).toEqual({
359
+ conversationId: "conv-old",
360
+ });
361
+ expect(
362
+ bootstrapStore.markActivationBootstrapSent({
363
+ platform: "openclaw",
364
+ accountId: "default",
365
+ conversationId: "conv-other",
366
+ }),
367
+ ).toBe(false);
368
+ expect(
369
+ bootstrapStore.markActivationBootstrapSent({
370
+ platform: "openclaw",
371
+ accountId: "default",
372
+ conversationId: "conv-old",
373
+ }),
374
+ ).toBe(true);
375
+ expect(
376
+ bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
377
+ ).toBeNull();
378
+ expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
379
+ conversation_id: "conv-old",
380
+ bootstrap_sent: 1,
381
+ });
382
+
383
+ store.upsertActivation({
384
+ platform: "openclaw",
385
+ accountId: "default",
386
+ userId: "user-new",
387
+ accessToken: "token-new",
388
+ refreshToken: null,
389
+ conversationId: "conv-new",
390
+ activatedAt: 2000,
391
+ loginMethod: "login",
392
+ });
393
+
394
+ expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
395
+ conversation_id: "conv-new",
396
+ bootstrap_sent: 0,
397
+ updated_at: 2000,
398
+ });
399
+ expect(store.getActivationConversation({ platform: "openclaw", accountId: "default" })).toMatchObject({
400
+ conversationId: "conv-new",
401
+ });
402
+ expect(
403
+ bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
404
+ ).toEqual({
405
+ conversationId: "conv-new",
406
+ });
407
+
408
+ store.close();
409
+ });
410
+
411
+ it("reclaims stale activation bootstrap claims after the timeout", () => {
412
+ vi.useFakeTimers();
413
+ vi.setSystemTime(10_000);
414
+ const store = createClawChatStore({ dbPath: tempDbPath() });
415
+ const bootstrapStore = store as typeof store & {
416
+ claimPendingActivationBootstrap(input: {
417
+ platform: string;
418
+ accountId: string;
419
+ staleClaimMs?: number;
420
+ }): { conversationId: string } | null;
421
+ };
422
+
423
+ store.upsertActivation({
424
+ platform: "openclaw",
425
+ accountId: "default",
426
+ userId: "user-new",
427
+ conversationId: "conv-stale",
428
+ activatedAt: 10_000,
429
+ loginMethod: "login",
430
+ });
431
+
432
+ expect(
433
+ bootstrapStore.claimPendingActivationBootstrap({
434
+ platform: "openclaw",
435
+ accountId: "default",
436
+ staleClaimMs: 1000,
437
+ }),
438
+ ).toEqual({ conversationId: "conv-stale" });
439
+ expect(
440
+ bootstrapStore.claimPendingActivationBootstrap({
441
+ platform: "openclaw",
442
+ accountId: "default",
443
+ staleClaimMs: 1000,
444
+ }),
445
+ ).toBeNull();
446
+
447
+ vi.setSystemTime(11_001);
448
+ expect(
449
+ bootstrapStore.claimPendingActivationBootstrap({
450
+ platform: "openclaw",
451
+ accountId: "default",
452
+ staleClaimMs: 1000,
453
+ }),
454
+ ).toEqual({ conversationId: "conv-stale" });
455
+
456
+ store.close();
457
+ });
458
+
459
+ it("upserts message-path conversation summaries without members or profiles", () => {
460
+ const store = createClawChatStore({ dbPath: tempDbPath() });
461
+
462
+ store.upsertConversationSummary({
463
+ platform: "openclaw",
464
+ accountId: "default",
465
+ conversationId: "conv-summary",
466
+ conversationType: "direct",
467
+ metadataVersion: 3,
468
+ lastSeenAt: 3000,
469
+ raw: { source: "message" },
470
+ });
471
+
472
+ expect(store.listCachedConversationIds({ platform: "openclaw", accountId: "default", limit: 10 })).toEqual([
473
+ "conv-summary",
474
+ ]);
475
+ expect(store.listConversationCacheForTest("clawchat_conversations")).toMatchObject([
476
+ {
477
+ conversation_id: "conv-summary",
478
+ conversation_type: "direct",
479
+ metadata_version: 3,
480
+ last_seen_at: 3000,
481
+ },
482
+ ]);
483
+ expect(store.listConversationCacheForTest("clawchat_conversation_members")).toEqual([]);
484
+
485
+ store.close();
486
+ });
487
+
488
+ it("upserts full conversation details and replaces members only for complete snapshots", () => {
489
+ const store = createClawChatStore({ dbPath: tempDbPath() });
490
+
491
+ store.upsertConversationDetails({
492
+ platform: "openclaw",
493
+ accountId: "default",
494
+ conversationId: "group-1",
495
+ conversationType: "group",
496
+ metadataVersion: 7,
497
+ lastSeenAt: 5000,
498
+ lastRefreshedAt: 5100,
499
+ raw: { id: "group-1" },
500
+ members: [
501
+ { userId: "user-a", role: "owner", raw: { role: "owner" }, lastSeenAt: 5001 },
502
+ { userId: "user-b", role: "member", raw: { role: "member" }, lastSeenAt: 5002 },
503
+ ],
504
+ membersComplete: true,
505
+ });
506
+ store.upsertConversationDetails({
507
+ platform: "openclaw",
508
+ accountId: "default",
509
+ conversationId: "group-1",
510
+ conversationType: "group",
511
+ lastSeenAt: 6000,
512
+ members: [{ userId: "user-c", role: "guest" }],
513
+ membersComplete: false,
514
+ });
515
+
516
+ expect(store.listConversationCacheForTest("clawchat_conversation_members")).toMatchObject([
517
+ { conversation_id: "group-1", user_id: "user-a", role: "owner" },
518
+ { conversation_id: "group-1", user_id: "user-b", role: "member" },
519
+ ]);
520
+
521
+ store.upsertConversationDetails({
522
+ platform: "openclaw",
523
+ accountId: "default",
524
+ conversationId: "group-1",
525
+ members: [{ userId: "user-c", role: "guest" }],
526
+ membersComplete: true,
527
+ });
528
+
529
+ expect(store.listConversationCacheForTest("clawchat_conversation_members")).toMatchObject([
530
+ { conversation_id: "group-1", user_id: "user-c", role: "guest" },
531
+ ]);
532
+
533
+ store.close();
534
+ });
535
+
536
+ it("preserves omitted cache fields and clears explicit null cache fields", () => {
537
+ const store = createClawChatStore({ dbPath: tempDbPath() });
538
+
539
+ store.upsertConversationDetails({
540
+ platform: "openclaw",
541
+ accountId: "default",
542
+ conversationId: "cache-clear",
543
+ conversationType: "group",
544
+ metadataVersion: 4,
545
+ raw: { old: "conversation" },
546
+ });
547
+
548
+ expect(store.listConversationCacheForTest("clawchat_conversations")).toMatchObject([
549
+ { conversation_type: "group", metadata_version: 4, raw_json: JSON.stringify({ old: "conversation" }) },
550
+ ]);
551
+
552
+ store.upsertConversationDetails({
553
+ platform: "openclaw",
554
+ accountId: "default",
555
+ conversationId: "cache-clear",
556
+ conversationType: null,
557
+ metadataVersion: null,
558
+ raw: null,
559
+ });
560
+
561
+ expect(store.listConversationCacheForTest("clawchat_conversations")).toMatchObject([
562
+ { conversation_type: null, metadata_version: null, raw_json: null },
563
+ ]);
564
+
565
+ store.close();
566
+ });
567
+
568
+ it("preserves omitted cache timestamps and clears explicit null cache timestamps", () => {
569
+ const store = createClawChatStore({ dbPath: tempDbPath() });
570
+
571
+ store.upsertConversationDetails({
572
+ platform: "openclaw",
573
+ accountId: "default",
574
+ conversationId: "cache-timestamps",
575
+ lastSeenAt: 1000,
576
+ lastRefreshedAt: 1100,
577
+ });
578
+
579
+ expect(store.listConversationCacheForTest("clawchat_conversations")).toMatchObject([
580
+ { last_seen_at: 1000, last_refreshed_at: 1100 },
581
+ ]);
582
+
583
+ store.upsertConversationDetails({
584
+ platform: "openclaw",
585
+ accountId: "default",
586
+ conversationId: "cache-timestamps",
587
+ lastSeenAt: null,
588
+ lastRefreshedAt: null,
589
+ });
590
+
591
+ expect(store.listConversationCacheForTest("clawchat_conversations")).toMatchObject([
592
+ { last_seen_at: null, last_refreshed_at: null },
593
+ ]);
594
+
595
+ store.close();
596
+ });
597
+
598
+ it("deletes conversation cache without deleting messages", () => {
599
+ const store = createClawChatStore({ dbPath: tempDbPath() });
600
+
601
+ store.upsertConversationDetails({
602
+ platform: "openclaw",
603
+ accountId: "default",
604
+ conversationId: "group-delete",
605
+ conversationType: "group",
606
+ members: [{ userId: "user-keep", role: "member" }],
607
+ membersComplete: true,
608
+ });
609
+ store.insertMessage({
610
+ platform: "openclaw",
611
+ accountId: "default",
612
+ kind: "message",
613
+ direction: "inbound",
614
+ eventType: "message.send",
615
+ chatId: "group-delete",
616
+ messageId: "msg-keep",
617
+ text: "keep me",
618
+ createdAt: 7000,
619
+ });
620
+
621
+ store.deleteConversationCache({ platform: "openclaw", accountId: "default", conversationId: "group-delete" });
622
+
623
+ expect(store.listConversationCacheForTest("clawchat_conversations")).toEqual([]);
624
+ expect(store.listConversationCacheForTest("clawchat_conversation_members")).toEqual([]);
625
+ expect(store.listMessagesForTest()).toMatchObject([{ chat_id: "group-delete", message_id: "msg-keep" }]);
626
+
627
+ store.close();
628
+ });
629
+
630
+ it("lists cached conversation ids by latest seen time with a limit", () => {
631
+ const store = createClawChatStore({ dbPath: tempDbPath() });
632
+
633
+ store.upsertConversationSummary({
634
+ platform: "openclaw",
635
+ accountId: "default",
636
+ conversationId: "old",
637
+ lastSeenAt: 1000,
638
+ });
639
+ store.upsertConversationSummary({
640
+ platform: "openclaw",
641
+ accountId: "default",
642
+ conversationId: "new",
643
+ lastSeenAt: 3000,
644
+ });
645
+ store.upsertConversationSummary({
646
+ platform: "openclaw",
647
+ accountId: "default",
648
+ conversationId: "middle",
649
+ lastSeenAt: 2000,
650
+ });
651
+
652
+ expect(store.listCachedConversationIds({ platform: "openclaw", accountId: "default", limit: 2 })).toEqual([
653
+ "new",
654
+ "middle",
655
+ ]);
656
+
657
+ store.close();
658
+ });
659
+
660
+ it("gets one cached conversation metadata row by id", () => {
661
+ const store = createClawChatStore({ dbPath: tempDbPath() });
662
+
663
+ store.upsertConversationSummary({
664
+ platform: "openclaw",
665
+ accountId: "default",
666
+ conversationId: "group-meta",
667
+ conversationType: "group",
668
+ metadataVersion: 9,
669
+ lastSeenAt: 4000,
670
+ lastRefreshedAt: 4100,
671
+ });
672
+
673
+ expect(store.getCachedConversation({
674
+ platform: "openclaw",
675
+ accountId: "default",
676
+ conversationId: "group-meta",
677
+ })).toMatchObject({
678
+ conversationId: "group-meta",
679
+ conversationType: "group",
680
+ metadataVersion: 9,
681
+ lastSeenAt: 4000,
682
+ lastRefreshedAt: 4100,
683
+ });
684
+ expect(store.getCachedConversation({
685
+ platform: "openclaw",
686
+ accountId: "default",
687
+ conversationId: "missing",
688
+ })).toBeNull();
689
+
690
+ store.close();
691
+ });
692
+ });