@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/storage.ts ADDED
@@ -0,0 +1,989 @@
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 { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
6
+
7
+ type Log = { error?: (message: string) => void };
8
+
9
+ export type ClawChatStoreOptions = {
10
+ dbPath?: string;
11
+ log?: Log;
12
+ };
13
+
14
+ export type ActivationInput = {
15
+ platform: string;
16
+ accountId: string;
17
+ userId?: string | null;
18
+ ownerUserId?: string | null;
19
+ accessToken?: string | null;
20
+ refreshToken?: string | null;
21
+ conversationId?: string | null;
22
+ activatedAt?: number;
23
+ loginMethod?: string | null;
24
+ };
25
+
26
+ export type ActivationBootstrapInput = {
27
+ platform: string;
28
+ accountId: string;
29
+ staleClaimMs?: number;
30
+ };
31
+
32
+ export type ActivationBootstrapConversation = { conversationId: string };
33
+
34
+ export type MarkActivationBootstrapSentInput = ActivationBootstrapInput & {
35
+ conversationId: string;
36
+ };
37
+
38
+ export type ReleaseActivationBootstrapClaimInput = ActivationBootstrapInput & {
39
+ conversationId: string;
40
+ };
41
+
42
+ export type MessageInput = {
43
+ platform: string;
44
+ accountId: string;
45
+ kind: string;
46
+ direction: string;
47
+ eventType: string;
48
+ traceId?: string | null;
49
+ chatId?: string | null;
50
+ messageId?: string | null;
51
+ text?: string | null;
52
+ raw?: unknown;
53
+ createdAt?: number;
54
+ };
55
+
56
+ export type StartConnectionInput = {
57
+ platform: string;
58
+ accountId: string;
59
+ attempt?: number | null;
60
+ reconnectCount?: number | null;
61
+ connectStartedAt?: number;
62
+ };
63
+
64
+ export type ConnectionUpdateOptions = { at?: number };
65
+
66
+ export type ConnectionReadyOptions = ConnectionUpdateOptions & {
67
+ resolvedDeviceId?: string | null;
68
+ deliveryMode?: string | null;
69
+ };
70
+
71
+ export type FinishConnectionInput = {
72
+ state: string;
73
+ disconnectedAt?: number;
74
+ closeCode?: number | null;
75
+ closeReason?: string | null;
76
+ error?: string | null;
77
+ };
78
+
79
+ export type ToolCallInput = {
80
+ platform: string;
81
+ accountId?: string | null;
82
+ toolName: string;
83
+ args?: unknown;
84
+ result?: unknown;
85
+ error?: string | null;
86
+ startedAt?: number;
87
+ endedAt?: number | null;
88
+ };
89
+
90
+ export type ConversationSummaryInput = {
91
+ platform: string;
92
+ accountId: string;
93
+ conversationId: string;
94
+ conversationType?: string | null;
95
+ metadataVersion?: number | null;
96
+ lastSeenAt?: number | null;
97
+ lastRefreshedAt?: number | null;
98
+ raw?: unknown;
99
+ };
100
+
101
+ export type ConversationMemberInput = {
102
+ userId: string;
103
+ role?: string | null;
104
+ raw?: unknown;
105
+ lastSeenAt?: number | null;
106
+ };
107
+
108
+ export type ConversationDetailsInput = ConversationSummaryInput & {
109
+ members?: ConversationMemberInput[];
110
+ membersComplete?: boolean;
111
+ };
112
+
113
+ export type ConversationAccountInput = {
114
+ platform: string;
115
+ accountId: string;
116
+ };
117
+
118
+ export type DeleteConversationCacheInput = ConversationAccountInput & {
119
+ conversationId: string;
120
+ };
121
+
122
+ export type ListCachedConversationIdsInput = ConversationAccountInput & {
123
+ limit?: number;
124
+ };
125
+
126
+ export type ActivationConversation = {
127
+ conversationId: string;
128
+ conversationType: string | null;
129
+ metadataVersion: number | null;
130
+ lastSeenAt: number | null;
131
+ lastRefreshedAt: number | null;
132
+ };
133
+
134
+ export type CachedConversation = ActivationConversation;
135
+
136
+ type Migration = { version: number; name: string; sql: string };
137
+
138
+ const DB_FILENAME = "clawchat.sqlite";
139
+ const DEFAULT_BOOTSTRAP_CLAIM_STALE_MS = 5 * 60 * 1000;
140
+
141
+ const MIGRATIONS: Migration[] = [
142
+ {
143
+ version: 1,
144
+ name: "initial_schema",
145
+ sql: `
146
+ CREATE TABLE IF NOT EXISTS schema_migrations (
147
+ version INTEGER PRIMARY KEY,
148
+ name TEXT NOT NULL,
149
+ applied_at INTEGER NOT NULL
150
+ );
151
+
152
+ CREATE TABLE IF NOT EXISTS clawchat_messages (
153
+ id INTEGER PRIMARY KEY,
154
+ platform TEXT NOT NULL,
155
+ account_id TEXT NOT NULL,
156
+ kind TEXT NOT NULL,
157
+ direction TEXT NOT NULL,
158
+ event_type TEXT NOT NULL,
159
+ trace_id TEXT,
160
+ chat_id TEXT,
161
+ message_id TEXT,
162
+ text TEXT,
163
+ raw_json TEXT,
164
+ created_at INTEGER NOT NULL
165
+ );
166
+
167
+ CREATE TABLE IF NOT EXISTS activations (
168
+ platform TEXT NOT NULL,
169
+ account_id TEXT NOT NULL,
170
+ user_id TEXT,
171
+ access_token TEXT,
172
+ refresh_token TEXT,
173
+ activated_at INTEGER NOT NULL,
174
+ login_method TEXT,
175
+ updated_at INTEGER NOT NULL,
176
+ PRIMARY KEY (platform, account_id)
177
+ );
178
+
179
+ CREATE TABLE IF NOT EXISTS connections (
180
+ id INTEGER PRIMARY KEY,
181
+ platform TEXT NOT NULL,
182
+ account_id TEXT NOT NULL,
183
+ attempt INTEGER,
184
+ reconnect_count INTEGER,
185
+ state TEXT NOT NULL,
186
+ connect_started_at INTEGER,
187
+ connect_sent_at INTEGER,
188
+ ready_at INTEGER,
189
+ disconnected_at INTEGER,
190
+ close_code INTEGER,
191
+ close_reason TEXT,
192
+ error TEXT,
193
+ created_at INTEGER NOT NULL,
194
+ updated_at INTEGER NOT NULL
195
+ );
196
+
197
+ CREATE TABLE IF NOT EXISTS tool_calls (
198
+ id INTEGER PRIMARY KEY,
199
+ platform TEXT NOT NULL,
200
+ account_id TEXT,
201
+ tool_name TEXT NOT NULL,
202
+ args_json TEXT,
203
+ result_json TEXT,
204
+ error TEXT,
205
+ started_at INTEGER NOT NULL,
206
+ ended_at INTEGER,
207
+ duration_ms INTEGER,
208
+ created_at INTEGER NOT NULL
209
+ );
210
+
211
+ CREATE INDEX IF NOT EXISTS idx_clawchat_messages_chat_created
212
+ ON clawchat_messages(chat_id, created_at);
213
+ CREATE INDEX IF NOT EXISTS idx_clawchat_messages_message_id
214
+ ON clawchat_messages(message_id);
215
+ CREATE INDEX IF NOT EXISTS idx_connections_account_created
216
+ ON connections(platform, account_id, created_at);
217
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_name_created
218
+ ON tool_calls(tool_name, created_at);
219
+ `,
220
+ },
221
+ {
222
+ version: 2,
223
+ name: "message_idempotency",
224
+ sql: `
225
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_clawchat_messages_message_unique
226
+ ON clawchat_messages(account_id, direction, kind, message_id)
227
+ WHERE kind = 'message' AND message_id IS NOT NULL;
228
+ `,
229
+ },
230
+ {
231
+ version: 3,
232
+ name: "activation_bootstrap",
233
+ sql: `
234
+ ALTER TABLE activations ADD COLUMN conversation_id TEXT;
235
+ ALTER TABLE activations ADD COLUMN bootstrap_sent INTEGER NOT NULL DEFAULT 1;
236
+ ALTER TABLE activations ADD COLUMN bootstrap_claimed_at INTEGER;
237
+ UPDATE activations SET access_token = NULL, refresh_token = NULL;
238
+ `,
239
+ },
240
+ {
241
+ version: 4,
242
+ name: "activation_owner_user_id",
243
+ sql: `
244
+ ALTER TABLE activations ADD COLUMN owner_user_id TEXT;
245
+ `,
246
+ },
247
+ {
248
+ version: 5,
249
+ name: "conversation_cache",
250
+ sql: `
251
+ ALTER TABLE connections ADD COLUMN resolved_device_id TEXT;
252
+ ALTER TABLE connections ADD COLUMN delivery_mode TEXT;
253
+
254
+ CREATE TABLE IF NOT EXISTS clawchat_conversations (
255
+ platform TEXT NOT NULL,
256
+ account_id TEXT NOT NULL,
257
+ conversation_id TEXT NOT NULL,
258
+ conversation_type TEXT,
259
+ metadata_version INTEGER,
260
+ last_seen_at INTEGER,
261
+ last_refreshed_at INTEGER,
262
+ raw_json TEXT,
263
+ PRIMARY KEY (platform, account_id, conversation_id)
264
+ );
265
+
266
+ CREATE TABLE IF NOT EXISTS clawchat_conversation_members (
267
+ platform TEXT NOT NULL,
268
+ account_id TEXT NOT NULL,
269
+ conversation_id TEXT NOT NULL,
270
+ user_id TEXT NOT NULL,
271
+ role TEXT,
272
+ raw_json TEXT,
273
+ last_seen_at INTEGER,
274
+ PRIMARY KEY (platform, account_id, conversation_id, user_id)
275
+ );
276
+
277
+ CREATE INDEX IF NOT EXISTS idx_clawchat_conversations_seen
278
+ ON clawchat_conversations(platform, account_id, last_seen_at);
279
+ `,
280
+ },
281
+ ];
282
+
283
+ function fallbackDbPath(): string {
284
+ const home = process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw");
285
+ return path.join(home, DB_FILENAME);
286
+ }
287
+
288
+ export function clawChatDbPathForStateDir(stateDir: string): string {
289
+ return path.join(stateDir, DB_FILENAME);
290
+ }
291
+
292
+ export function defaultDbPath(): string {
293
+ try {
294
+ return clawChatDbPathForStateDir(resolveStateDir());
295
+ } catch {
296
+ return fallbackDbPath();
297
+ }
298
+ }
299
+
300
+ function toNullableString(value: unknown): string | null {
301
+ return typeof value === "string" ? value : value == null ? null : String(value);
302
+ }
303
+
304
+ function toJson(value: unknown): string | null {
305
+ if (value === undefined) return null;
306
+ try {
307
+ return JSON.stringify(value);
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+
313
+ function toCacheJson(value: unknown): string | null {
314
+ if (value == null) return null;
315
+ return toJson(value);
316
+ }
317
+
318
+ function hasOwn(value: object, key: PropertyKey): 0 | 1 {
319
+ return Object.prototype.hasOwnProperty.call(value, key) ? 1 : 0;
320
+ }
321
+
322
+ function safeErrorMessage(err: unknown): string {
323
+ const message = err instanceof Error ? err.message : String(err);
324
+ return message
325
+ .replace(/(access[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
326
+ .replace(/(refresh[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
327
+ .replace(/(authorization["'\s:=]+bearer\s+)[^"'\s,}]+/gi, "$1[REDACTED]");
328
+ }
329
+
330
+ export class ClawChatStore {
331
+ readonly dbPath: string;
332
+ private readonly log?: Log;
333
+ private db: DatabaseSync | null = null;
334
+ private initialized = false;
335
+ private disabled = false;
336
+
337
+ constructor(options: ClawChatStoreOptions = {}) {
338
+ this.dbPath = options.dbPath ?? defaultDbPath();
339
+ this.log = options.log;
340
+ }
341
+
342
+ initialize(): void {
343
+ this.ensureInitialized();
344
+ }
345
+
346
+ listAppliedMigrations(): Array<{ version: number; name: string }> {
347
+ return this.read(() =>
348
+ this.requireDb()
349
+ .prepare("SELECT version, name FROM schema_migrations ORDER BY version")
350
+ .all() as Array<{ version: number; name: string }>,
351
+ ) ?? [];
352
+ }
353
+
354
+ upsertActivation(input: ActivationInput): void {
355
+ this.write(() => {
356
+ const now = input.activatedAt ?? Date.now();
357
+ const conversationId = input.conversationId?.trim() || null;
358
+ const userId = input.userId?.trim() || null;
359
+ const ownerUserId = input.ownerUserId?.trim() || null;
360
+ this.requireDb()
361
+ .prepare(
362
+ `INSERT INTO activations(
363
+ platform, account_id, user_id, owner_user_id, access_token, refresh_token,
364
+ activated_at, login_method, conversation_id, bootstrap_sent,
365
+ bootstrap_claimed_at, updated_at
366
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
367
+ ON CONFLICT(platform, account_id) DO UPDATE SET
368
+ user_id = excluded.user_id,
369
+ owner_user_id = excluded.owner_user_id,
370
+ access_token = NULL,
371
+ refresh_token = NULL,
372
+ activated_at = excluded.activated_at,
373
+ login_method = excluded.login_method,
374
+ conversation_id = excluded.conversation_id,
375
+ bootstrap_sent = excluded.bootstrap_sent,
376
+ bootstrap_claimed_at = NULL,
377
+ updated_at = excluded.updated_at`,
378
+ )
379
+ .run(
380
+ input.platform,
381
+ input.accountId,
382
+ userId,
383
+ ownerUserId,
384
+ null,
385
+ null,
386
+ now,
387
+ input.loginMethod ?? null,
388
+ conversationId,
389
+ conversationId ? 0 : 1,
390
+ null,
391
+ now,
392
+ );
393
+ if (conversationId) {
394
+ this.upsertConversationSummaryInDb({
395
+ platform: input.platform,
396
+ accountId: input.accountId,
397
+ conversationId,
398
+ lastSeenAt: now,
399
+ });
400
+ }
401
+ });
402
+ }
403
+
404
+ upsertConversationSummary(input: ConversationSummaryInput): void {
405
+ this.write(() => {
406
+ this.upsertConversationSummaryInDb(input);
407
+ });
408
+ }
409
+
410
+ upsertConversationDetails(input: ConversationDetailsInput): void {
411
+ this.write(() => {
412
+ const db = this.requireDb();
413
+ db.exec("BEGIN");
414
+ try {
415
+ this.upsertConversationSummaryInDb(input);
416
+ if (input.membersComplete) {
417
+ db.prepare(
418
+ `DELETE FROM clawchat_conversation_members
419
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`,
420
+ ).run(input.platform, input.accountId, input.conversationId);
421
+ for (const member of input.members ?? []) {
422
+ db.prepare(
423
+ `INSERT INTO clawchat_conversation_members(
424
+ platform, account_id, conversation_id, user_id, role, raw_json, last_seen_at
425
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
426
+ ).run(
427
+ input.platform,
428
+ input.accountId,
429
+ input.conversationId,
430
+ member.userId,
431
+ member.role ?? null,
432
+ toJson(member.raw),
433
+ member.lastSeenAt ?? null,
434
+ );
435
+ }
436
+ }
437
+ db.exec("COMMIT");
438
+ } catch (err) {
439
+ db.exec("ROLLBACK");
440
+ throw err;
441
+ }
442
+ });
443
+ }
444
+
445
+ deleteConversationCache(input: DeleteConversationCacheInput): void {
446
+ this.write(() => {
447
+ const db = this.requireDb();
448
+ db.prepare(
449
+ `DELETE FROM clawchat_conversation_members
450
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`,
451
+ ).run(input.platform, input.accountId, input.conversationId);
452
+ db.prepare(
453
+ `DELETE FROM clawchat_conversations
454
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`,
455
+ ).run(input.platform, input.accountId, input.conversationId);
456
+ });
457
+ }
458
+
459
+ listCachedConversationIds(input: ListCachedConversationIdsInput): string[] {
460
+ const limit = Math.min(100, Math.max(0, Math.trunc(input.limit ?? 50)));
461
+ return this.read(() =>
462
+ this.requireDb()
463
+ .prepare(
464
+ `SELECT conversation_id FROM clawchat_conversations
465
+ WHERE platform = ? AND account_id = ?
466
+ ORDER BY last_seen_at DESC, conversation_id ASC
467
+ LIMIT ?`,
468
+ )
469
+ .all(input.platform, input.accountId, limit)
470
+ .map((row) => String((row as { conversation_id: unknown }).conversation_id)),
471
+ ) ?? [];
472
+ }
473
+
474
+ getCachedConversation(input: DeleteConversationCacheInput): CachedConversation | null {
475
+ return this.read(() => {
476
+ const row = this.requireDb()
477
+ .prepare(
478
+ `SELECT conversation_id, conversation_type, metadata_version, last_seen_at, last_refreshed_at
479
+ FROM clawchat_conversations
480
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`,
481
+ )
482
+ .get(input.platform, input.accountId, input.conversationId) as
483
+ | {
484
+ conversation_id?: unknown;
485
+ conversation_type?: unknown;
486
+ metadata_version?: unknown;
487
+ last_seen_at?: unknown;
488
+ last_refreshed_at?: unknown;
489
+ }
490
+ | undefined;
491
+ if (typeof row?.conversation_id !== "string") return null;
492
+ return {
493
+ conversationId: row.conversation_id,
494
+ conversationType: typeof row.conversation_type === "string" ? row.conversation_type : null,
495
+ metadataVersion: typeof row.metadata_version === "number" ? row.metadata_version : null,
496
+ lastSeenAt: typeof row.last_seen_at === "number" ? row.last_seen_at : null,
497
+ lastRefreshedAt: typeof row.last_refreshed_at === "number" ? row.last_refreshed_at : null,
498
+ };
499
+ }) ?? null;
500
+ }
501
+
502
+ getActivationConversation(input: ConversationAccountInput): ActivationConversation | null {
503
+ return this.read(() => {
504
+ const row = this.requireDb()
505
+ .prepare(
506
+ `SELECT
507
+ a.conversation_id,
508
+ c.conversation_type,
509
+ c.metadata_version,
510
+ c.last_seen_at,
511
+ c.last_refreshed_at
512
+ FROM activations a
513
+ LEFT JOIN clawchat_conversations c
514
+ ON c.platform = a.platform
515
+ AND c.account_id = a.account_id
516
+ AND c.conversation_id = a.conversation_id
517
+ WHERE a.platform = ?
518
+ AND a.account_id = ?
519
+ AND a.conversation_id IS NOT NULL
520
+ AND a.conversation_id <> ''`,
521
+ )
522
+ .get(input.platform, input.accountId) as
523
+ | {
524
+ conversation_id?: unknown;
525
+ conversation_type?: unknown;
526
+ metadata_version?: unknown;
527
+ last_seen_at?: unknown;
528
+ last_refreshed_at?: unknown;
529
+ }
530
+ | undefined;
531
+ if (typeof row?.conversation_id !== "string") return null;
532
+ return {
533
+ conversationId: row.conversation_id,
534
+ conversationType: typeof row.conversation_type === "string" ? row.conversation_type : null,
535
+ metadataVersion: typeof row.metadata_version === "number" ? row.metadata_version : null,
536
+ lastSeenAt: typeof row.last_seen_at === "number" ? row.last_seen_at : null,
537
+ lastRefreshedAt: typeof row.last_refreshed_at === "number" ? row.last_refreshed_at : null,
538
+ };
539
+ }) ?? null;
540
+ }
541
+
542
+ claimPendingActivationBootstrap(
543
+ input: ActivationBootstrapInput,
544
+ ): ActivationBootstrapConversation | null {
545
+ return this.write(() => {
546
+ const now = Date.now();
547
+ const staleMs = Math.max(0, input.staleClaimMs ?? DEFAULT_BOOTSTRAP_CLAIM_STALE_MS);
548
+ const staleBefore = now - staleMs;
549
+ const row = this.requireDb()
550
+ .prepare(
551
+ `UPDATE activations SET
552
+ bootstrap_claimed_at = ?, updated_at = ?
553
+ WHERE platform = ?
554
+ AND account_id = ?
555
+ AND conversation_id IS NOT NULL
556
+ AND conversation_id <> ''
557
+ AND bootstrap_sent = 0
558
+ AND (bootstrap_claimed_at IS NULL OR bootstrap_claimed_at <= ?)
559
+ RETURNING conversation_id`,
560
+ )
561
+ .get(now, now, input.platform, input.accountId, staleBefore) as
562
+ | { conversation_id?: unknown }
563
+ | undefined;
564
+ const conversationId = row?.conversation_id;
565
+ return typeof conversationId === "string" && conversationId.length > 0
566
+ ? { conversationId }
567
+ : null;
568
+ }) ?? null;
569
+ }
570
+
571
+ releaseActivationBootstrapClaim(input: ReleaseActivationBootstrapClaimInput): boolean | null {
572
+ return this.write(() => {
573
+ const now = Date.now();
574
+ const result = this.requireDb()
575
+ .prepare(
576
+ `UPDATE activations SET
577
+ bootstrap_claimed_at = NULL,
578
+ updated_at = ?
579
+ WHERE platform = ?
580
+ AND account_id = ?
581
+ AND conversation_id = ?
582
+ AND bootstrap_sent = 0
583
+ AND bootstrap_claimed_at IS NOT NULL`,
584
+ )
585
+ .run(now, input.platform, input.accountId, input.conversationId);
586
+ return result.changes > 0;
587
+ });
588
+ }
589
+
590
+ markActivationBootstrapSent(input: MarkActivationBootstrapSentInput): boolean | null {
591
+ return this.write(() => {
592
+ const now = Date.now();
593
+ const result = this.requireDb()
594
+ .prepare(
595
+ `UPDATE activations SET
596
+ bootstrap_sent = 1,
597
+ bootstrap_claimed_at = NULL,
598
+ updated_at = ?
599
+ WHERE platform = ?
600
+ AND account_id = ?
601
+ AND conversation_id = ?
602
+ AND bootstrap_sent = 0
603
+ AND bootstrap_claimed_at IS NOT NULL`,
604
+ )
605
+ .run(now, input.platform, input.accountId, input.conversationId);
606
+ return result.changes > 0;
607
+ });
608
+ }
609
+
610
+ insertMessage(input: MessageInput): boolean | null {
611
+ return this.write(() => {
612
+ this.requireDb()
613
+ .prepare(
614
+ `INSERT INTO clawchat_messages(
615
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
616
+ message_id, text, raw_json, created_at
617
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
618
+ )
619
+ .run(
620
+ input.platform,
621
+ input.accountId,
622
+ input.kind,
623
+ input.direction,
624
+ input.eventType,
625
+ input.traceId ?? null,
626
+ input.chatId ?? null,
627
+ input.messageId ?? null,
628
+ input.text ?? null,
629
+ toJson(input.raw),
630
+ input.createdAt ?? Date.now(),
631
+ );
632
+ return true;
633
+ });
634
+ }
635
+
636
+ claimMessageOnce(input: MessageInput): true | false | null {
637
+ return this.write(() => {
638
+ const result = this.requireDb()
639
+ .prepare(
640
+ `INSERT OR IGNORE INTO clawchat_messages(
641
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
642
+ message_id, text, raw_json, created_at
643
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
644
+ )
645
+ .run(
646
+ input.platform,
647
+ input.accountId,
648
+ input.kind,
649
+ input.direction,
650
+ input.eventType,
651
+ input.traceId ?? null,
652
+ input.chatId ?? null,
653
+ input.messageId ?? null,
654
+ input.text ?? null,
655
+ toJson(input.raw),
656
+ input.createdAt ?? Date.now(),
657
+ );
658
+ return result.changes > 0;
659
+ });
660
+ }
661
+
662
+ updateMessageByIdentity(input: {
663
+ accountId: string;
664
+ kind: string;
665
+ direction: MessageInput["direction"];
666
+ messageId: string;
667
+ eventType: string;
668
+ traceId?: string | null;
669
+ chatId?: string | null;
670
+ text?: string | null;
671
+ raw?: unknown;
672
+ }): void {
673
+ this.write(() => {
674
+ this.requireDb()
675
+ .prepare(
676
+ `UPDATE clawchat_messages SET
677
+ event_type = ?, trace_id = ?, chat_id = ?, text = ?, raw_json = ?
678
+ WHERE account_id = ? AND kind = ? AND direction = ? AND message_id = ?`,
679
+ )
680
+ .run(
681
+ input.eventType,
682
+ input.traceId ?? null,
683
+ input.chatId ?? null,
684
+ input.text ?? null,
685
+ toJson(input.raw),
686
+ input.accountId,
687
+ input.kind,
688
+ input.direction,
689
+ input.messageId,
690
+ );
691
+ });
692
+ }
693
+
694
+ startConnection(input: StartConnectionInput): number | null {
695
+ return this.write(() => {
696
+ const now = input.connectStartedAt ?? Date.now();
697
+ const result = this.requireDb()
698
+ .prepare(
699
+ `INSERT INTO connections(
700
+ platform, account_id, attempt, reconnect_count, state,
701
+ connect_started_at, created_at, updated_at
702
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
703
+ )
704
+ .run(
705
+ input.platform,
706
+ input.accountId,
707
+ input.attempt ?? null,
708
+ input.reconnectCount ?? null,
709
+ "connecting",
710
+ now,
711
+ now,
712
+ now,
713
+ );
714
+ return Number(result.lastInsertRowid);
715
+ }) ?? null;
716
+ }
717
+
718
+ markConnectSent(connectionId: number | null | undefined, options: ConnectionUpdateOptions = {}): void {
719
+ if (typeof connectionId !== "number") return;
720
+ this.updateConnectionTime(connectionId, "connect_sent_at", options.at ?? Date.now());
721
+ }
722
+
723
+ markConnectionReady(connectionId: number | null | undefined, options: ConnectionReadyOptions = {}): void {
724
+ if (typeof connectionId !== "number") return;
725
+ this.write(() => {
726
+ const now = options.at ?? Date.now();
727
+ this.requireDb()
728
+ .prepare(
729
+ `UPDATE connections SET
730
+ state = ?,
731
+ ready_at = ?,
732
+ resolved_device_id = CASE WHEN ? THEN ? ELSE resolved_device_id END,
733
+ delivery_mode = CASE WHEN ? THEN ? ELSE delivery_mode END,
734
+ updated_at = ?
735
+ WHERE id = ?`,
736
+ )
737
+ .run(
738
+ "ready",
739
+ now,
740
+ hasOwn(options, "resolvedDeviceId"),
741
+ options.resolvedDeviceId ?? null,
742
+ hasOwn(options, "deliveryMode"),
743
+ options.deliveryMode ?? null,
744
+ now,
745
+ connectionId,
746
+ );
747
+ });
748
+ }
749
+
750
+ finishConnection(connectionId: number | null | undefined, input: FinishConnectionInput): void {
751
+ if (typeof connectionId !== "number") return;
752
+ this.write(() => {
753
+ const now = input.disconnectedAt ?? Date.now();
754
+ this.requireDb()
755
+ .prepare(
756
+ `UPDATE connections SET
757
+ state = ?, disconnected_at = ?, close_code = ?, close_reason = ?, error = ?, updated_at = ?
758
+ WHERE id = ?`,
759
+ )
760
+ .run(
761
+ input.state,
762
+ now,
763
+ input.closeCode ?? null,
764
+ input.closeReason ?? null,
765
+ input.error ?? null,
766
+ now,
767
+ connectionId,
768
+ );
769
+ });
770
+ }
771
+
772
+ recordToolCall(input: ToolCallInput): void {
773
+ this.write(() => {
774
+ const startedAt = input.startedAt ?? Date.now();
775
+ const endedAt = input.endedAt ?? null;
776
+ const durationMs = endedAt == null ? null : Math.max(0, endedAt - startedAt);
777
+ this.requireDb()
778
+ .prepare(
779
+ `INSERT INTO tool_calls(
780
+ platform, account_id, tool_name, args_json, result_json, error,
781
+ started_at, ended_at, duration_ms, created_at
782
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
783
+ )
784
+ .run(
785
+ input.platform,
786
+ input.accountId ?? null,
787
+ input.toolName,
788
+ toJson(input.args),
789
+ toJson(input.result),
790
+ toNullableString(input.error),
791
+ startedAt,
792
+ endedAt,
793
+ durationMs,
794
+ startedAt,
795
+ );
796
+ });
797
+ }
798
+
799
+ getActivationForTest(platform: string, accountId: string): Record<string, unknown> | null {
800
+ return this.read(() =>
801
+ this.requireDb()
802
+ .prepare("SELECT * FROM activations WHERE platform = ? AND account_id = ?")
803
+ .get(platform, accountId) as Record<string, unknown> | undefined,
804
+ ) ?? null;
805
+ }
806
+
807
+ listMessagesForTest(): Record<string, unknown>[] {
808
+ return this.read(() =>
809
+ this.requireDb()
810
+ .prepare("SELECT * FROM clawchat_messages ORDER BY id")
811
+ .all() as Record<string, unknown>[],
812
+ ) ?? [];
813
+ }
814
+
815
+ listConnectionsForTest(): Record<string, unknown>[] {
816
+ return this.read(() =>
817
+ this.requireDb().prepare("SELECT * FROM connections ORDER BY id").all() as Record<string, unknown>[],
818
+ ) ?? [];
819
+ }
820
+
821
+ listToolCallsForTest(): Record<string, unknown>[] {
822
+ return this.read(() =>
823
+ this.requireDb().prepare("SELECT * FROM tool_calls ORDER BY id").all() as Record<string, unknown>[],
824
+ ) ?? [];
825
+ }
826
+
827
+ listConversationCacheForTest(table: string): Record<string, unknown>[] {
828
+ if (
829
+ ![
830
+ "clawchat_conversations",
831
+ "clawchat_conversation_members",
832
+ ].includes(table)
833
+ ) {
834
+ throw new Error(`unsupported conversation cache table: ${table}`);
835
+ }
836
+ return this.read(() =>
837
+ this.requireDb().prepare(`SELECT * FROM ${table} ORDER BY rowid`).all() as Record<string, unknown>[],
838
+ ) ?? [];
839
+ }
840
+
841
+ close(): void {
842
+ this.db?.close();
843
+ this.db = null;
844
+ this.initialized = false;
845
+ }
846
+
847
+ private updateConnectionTime(connectionId: number, field: "connect_sent_at", at: number): void {
848
+ this.write(() => {
849
+ this.requireDb()
850
+ .prepare(`UPDATE connections SET ${field} = ?, updated_at = ? WHERE id = ?`)
851
+ .run(at, at, connectionId);
852
+ });
853
+ }
854
+
855
+ private upsertConversationSummaryInDb(input: ConversationSummaryInput): void {
856
+ this.requireDb()
857
+ .prepare(
858
+ `INSERT INTO clawchat_conversations(
859
+ platform, account_id, conversation_id, conversation_type, metadata_version,
860
+ last_seen_at, last_refreshed_at, raw_json
861
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
862
+ ON CONFLICT(platform, account_id, conversation_id) DO UPDATE SET
863
+ conversation_type = CASE WHEN ? THEN excluded.conversation_type ELSE clawchat_conversations.conversation_type END,
864
+ metadata_version = CASE WHEN ? THEN excluded.metadata_version ELSE clawchat_conversations.metadata_version END,
865
+ last_seen_at = CASE WHEN ? THEN excluded.last_seen_at ELSE clawchat_conversations.last_seen_at END,
866
+ last_refreshed_at = CASE WHEN ? THEN excluded.last_refreshed_at ELSE clawchat_conversations.last_refreshed_at END,
867
+ raw_json = CASE WHEN ? THEN excluded.raw_json ELSE clawchat_conversations.raw_json END`,
868
+ )
869
+ .run(
870
+ input.platform,
871
+ input.accountId,
872
+ input.conversationId,
873
+ input.conversationType ?? null,
874
+ input.metadataVersion ?? null,
875
+ input.lastSeenAt ?? null,
876
+ input.lastRefreshedAt ?? null,
877
+ toCacheJson(input.raw),
878
+ hasOwn(input, "conversationType"),
879
+ hasOwn(input, "metadataVersion"),
880
+ hasOwn(input, "lastSeenAt"),
881
+ hasOwn(input, "lastRefreshedAt"),
882
+ hasOwn(input, "raw"),
883
+ );
884
+ }
885
+
886
+ private read<T>(fn: () => T): T | null {
887
+ if (!this.ensureInitialized()) return null;
888
+ try {
889
+ return fn();
890
+ } catch (err) {
891
+ this.logFailure("openclaw-clawchat sqlite read failed", err);
892
+ return null;
893
+ }
894
+ }
895
+
896
+ private write<T>(fn: () => T): T | null {
897
+ if (!this.ensureInitialized()) return null;
898
+ try {
899
+ return fn();
900
+ } catch (err) {
901
+ this.logFailure("openclaw-clawchat sqlite write failed", err);
902
+ return null;
903
+ }
904
+ }
905
+
906
+ private ensureInitialized(): boolean {
907
+ if (this.disabled) return false;
908
+ if (this.initialized) return true;
909
+ try {
910
+ fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
911
+ this.db = new DatabaseSync(this.dbPath);
912
+ this.db.exec("PRAGMA journal_mode=WAL");
913
+ try {
914
+ fs.chmodSync(this.dbPath, 0o600);
915
+ } catch {
916
+ // Best effort only; permissions vary by platform/filesystem.
917
+ }
918
+ this.applyMigrations();
919
+ this.initialized = true;
920
+ return true;
921
+ } catch (err) {
922
+ this.disabled = true;
923
+ this.logFailure("openclaw-clawchat sqlite disabled after initialization failure", err);
924
+ try {
925
+ this.db?.close();
926
+ } catch {
927
+ // best effort
928
+ }
929
+ this.db = null;
930
+ return false;
931
+ }
932
+ }
933
+
934
+ private applyMigrations(): void {
935
+ const db = this.requireDb();
936
+ db.exec(
937
+ `CREATE TABLE IF NOT EXISTS schema_migrations (
938
+ version INTEGER PRIMARY KEY,
939
+ name TEXT NOT NULL,
940
+ applied_at INTEGER NOT NULL
941
+ )`,
942
+ );
943
+ const hasMigration = db.prepare("SELECT 1 FROM schema_migrations WHERE version = ?");
944
+ const insertMigration = db.prepare(
945
+ "INSERT INTO schema_migrations(version, name, applied_at) VALUES (?, ?, ?)",
946
+ );
947
+ for (const migration of MIGRATIONS) {
948
+ if (hasMigration.get(migration.version)) continue;
949
+ db.exec("BEGIN");
950
+ try {
951
+ db.exec(migration.sql);
952
+ insertMigration.run(migration.version, migration.name, Date.now());
953
+ db.exec("COMMIT");
954
+ } catch (err) {
955
+ db.exec("ROLLBACK");
956
+ throw err;
957
+ }
958
+ }
959
+ }
960
+
961
+ private requireDb(): DatabaseSync {
962
+ if (!this.db) throw new Error("clawchat sqlite database is not open");
963
+ return this.db;
964
+ }
965
+
966
+ private logFailure(prefix: string, err: unknown): void {
967
+ this.log?.error?.(`${prefix}: ${safeErrorMessage(err)}`);
968
+ }
969
+ }
970
+
971
+ let singleton: ClawChatStore | null = null;
972
+
973
+ export function createClawChatStore(options: ClawChatStoreOptions = {}): ClawChatStore {
974
+ return new ClawChatStore(options);
975
+ }
976
+
977
+ export function getClawChatStore(options: ClawChatStoreOptions = {}): ClawChatStore {
978
+ const dbPath = options.dbPath ?? defaultDbPath();
979
+ if (!singleton || singleton.dbPath !== dbPath) {
980
+ singleton?.close();
981
+ singleton = createClawChatStore({ ...options, dbPath });
982
+ }
983
+ return singleton;
984
+ }
985
+
986
+ export function resetClawChatStoreForTest(): void {
987
+ singleton?.close();
988
+ singleton = null;
989
+ }