@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,636 @@
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
+ const DB_FILENAME = "clawchat.sqlite";
7
+ const DEFAULT_BOOTSTRAP_CLAIM_STALE_MS = 5 * 60 * 1000;
8
+ const MIGRATIONS = [
9
+ {
10
+ version: 1,
11
+ name: "initial_schema",
12
+ sql: `
13
+ CREATE TABLE IF NOT EXISTS schema_migrations (
14
+ version INTEGER PRIMARY KEY,
15
+ name TEXT NOT NULL,
16
+ applied_at INTEGER NOT NULL
17
+ );
18
+
19
+ CREATE TABLE IF NOT EXISTS clawchat_messages (
20
+ id INTEGER PRIMARY KEY,
21
+ platform TEXT NOT NULL,
22
+ account_id TEXT NOT NULL,
23
+ kind TEXT NOT NULL,
24
+ direction TEXT NOT NULL,
25
+ event_type TEXT NOT NULL,
26
+ trace_id TEXT,
27
+ chat_id TEXT,
28
+ message_id TEXT,
29
+ text TEXT,
30
+ raw_json TEXT,
31
+ created_at INTEGER NOT NULL
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS activations (
35
+ platform TEXT NOT NULL,
36
+ account_id TEXT NOT NULL,
37
+ user_id TEXT,
38
+ access_token TEXT,
39
+ refresh_token TEXT,
40
+ activated_at INTEGER NOT NULL,
41
+ login_method TEXT,
42
+ updated_at INTEGER NOT NULL,
43
+ PRIMARY KEY (platform, account_id)
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS connections (
47
+ id INTEGER PRIMARY KEY,
48
+ platform TEXT NOT NULL,
49
+ account_id TEXT NOT NULL,
50
+ attempt INTEGER,
51
+ reconnect_count INTEGER,
52
+ state TEXT NOT NULL,
53
+ connect_started_at INTEGER,
54
+ connect_sent_at INTEGER,
55
+ ready_at INTEGER,
56
+ disconnected_at INTEGER,
57
+ close_code INTEGER,
58
+ close_reason TEXT,
59
+ error TEXT,
60
+ created_at INTEGER NOT NULL,
61
+ updated_at INTEGER NOT NULL
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS tool_calls (
65
+ id INTEGER PRIMARY KEY,
66
+ platform TEXT NOT NULL,
67
+ account_id TEXT,
68
+ tool_name TEXT NOT NULL,
69
+ args_json TEXT,
70
+ result_json TEXT,
71
+ error TEXT,
72
+ started_at INTEGER NOT NULL,
73
+ ended_at INTEGER,
74
+ duration_ms INTEGER,
75
+ created_at INTEGER NOT NULL
76
+ );
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_clawchat_messages_chat_created
79
+ ON clawchat_messages(chat_id, created_at);
80
+ CREATE INDEX IF NOT EXISTS idx_clawchat_messages_message_id
81
+ ON clawchat_messages(message_id);
82
+ CREATE INDEX IF NOT EXISTS idx_connections_account_created
83
+ ON connections(platform, account_id, created_at);
84
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_name_created
85
+ ON tool_calls(tool_name, created_at);
86
+ `,
87
+ },
88
+ {
89
+ version: 2,
90
+ name: "message_idempotency",
91
+ sql: `
92
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_clawchat_messages_message_unique
93
+ ON clawchat_messages(account_id, direction, kind, message_id)
94
+ WHERE kind = 'message' AND message_id IS NOT NULL;
95
+ `,
96
+ },
97
+ {
98
+ version: 3,
99
+ name: "activation_bootstrap",
100
+ sql: `
101
+ ALTER TABLE activations ADD COLUMN conversation_id TEXT;
102
+ ALTER TABLE activations ADD COLUMN bootstrap_sent INTEGER NOT NULL DEFAULT 1;
103
+ ALTER TABLE activations ADD COLUMN bootstrap_claimed_at INTEGER;
104
+ UPDATE activations SET access_token = NULL, refresh_token = NULL;
105
+ `,
106
+ },
107
+ {
108
+ version: 4,
109
+ name: "activation_owner_user_id",
110
+ sql: `
111
+ ALTER TABLE activations ADD COLUMN owner_user_id TEXT;
112
+ `,
113
+ },
114
+ {
115
+ version: 5,
116
+ name: "conversation_cache",
117
+ sql: `
118
+ ALTER TABLE connections ADD COLUMN resolved_device_id TEXT;
119
+ ALTER TABLE connections ADD COLUMN delivery_mode TEXT;
120
+
121
+ CREATE TABLE IF NOT EXISTS clawchat_conversations (
122
+ platform TEXT NOT NULL,
123
+ account_id TEXT NOT NULL,
124
+ conversation_id TEXT NOT NULL,
125
+ conversation_type TEXT,
126
+ metadata_version INTEGER,
127
+ last_seen_at INTEGER,
128
+ last_refreshed_at INTEGER,
129
+ raw_json TEXT,
130
+ PRIMARY KEY (platform, account_id, conversation_id)
131
+ );
132
+
133
+ CREATE TABLE IF NOT EXISTS clawchat_conversation_members (
134
+ platform TEXT NOT NULL,
135
+ account_id TEXT NOT NULL,
136
+ conversation_id TEXT NOT NULL,
137
+ user_id TEXT NOT NULL,
138
+ role TEXT,
139
+ raw_json TEXT,
140
+ last_seen_at INTEGER,
141
+ PRIMARY KEY (platform, account_id, conversation_id, user_id)
142
+ );
143
+
144
+ CREATE INDEX IF NOT EXISTS idx_clawchat_conversations_seen
145
+ ON clawchat_conversations(platform, account_id, last_seen_at);
146
+ `,
147
+ },
148
+ ];
149
+ function fallbackDbPath() {
150
+ const home = process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw");
151
+ return path.join(home, DB_FILENAME);
152
+ }
153
+ export function clawChatDbPathForStateDir(stateDir) {
154
+ return path.join(stateDir, DB_FILENAME);
155
+ }
156
+ export function defaultDbPath() {
157
+ try {
158
+ return clawChatDbPathForStateDir(resolveStateDir());
159
+ }
160
+ catch {
161
+ return fallbackDbPath();
162
+ }
163
+ }
164
+ function toNullableString(value) {
165
+ return typeof value === "string" ? value : value == null ? null : String(value);
166
+ }
167
+ function toJson(value) {
168
+ if (value === undefined)
169
+ return null;
170
+ try {
171
+ return JSON.stringify(value);
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
177
+ function toCacheJson(value) {
178
+ if (value == null)
179
+ return null;
180
+ return toJson(value);
181
+ }
182
+ function hasOwn(value, key) {
183
+ return Object.prototype.hasOwnProperty.call(value, key) ? 1 : 0;
184
+ }
185
+ function safeErrorMessage(err) {
186
+ const message = err instanceof Error ? err.message : String(err);
187
+ return message
188
+ .replace(/(access[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
189
+ .replace(/(refresh[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
190
+ .replace(/(authorization["'\s:=]+bearer\s+)[^"'\s,}]+/gi, "$1[REDACTED]");
191
+ }
192
+ export class ClawChatStore {
193
+ dbPath;
194
+ log;
195
+ db = null;
196
+ initialized = false;
197
+ disabled = false;
198
+ constructor(options = {}) {
199
+ this.dbPath = options.dbPath ?? defaultDbPath();
200
+ this.log = options.log;
201
+ }
202
+ initialize() {
203
+ this.ensureInitialized();
204
+ }
205
+ listAppliedMigrations() {
206
+ return this.read(() => this.requireDb()
207
+ .prepare("SELECT version, name FROM schema_migrations ORDER BY version")
208
+ .all()) ?? [];
209
+ }
210
+ upsertActivation(input) {
211
+ this.write(() => {
212
+ const now = input.activatedAt ?? Date.now();
213
+ const conversationId = input.conversationId?.trim() || null;
214
+ const userId = input.userId?.trim() || null;
215
+ const ownerUserId = input.ownerUserId?.trim() || null;
216
+ this.requireDb()
217
+ .prepare(`INSERT INTO activations(
218
+ platform, account_id, user_id, owner_user_id, access_token, refresh_token,
219
+ activated_at, login_method, conversation_id, bootstrap_sent,
220
+ bootstrap_claimed_at, updated_at
221
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
222
+ ON CONFLICT(platform, account_id) DO UPDATE SET
223
+ user_id = excluded.user_id,
224
+ owner_user_id = excluded.owner_user_id,
225
+ access_token = NULL,
226
+ refresh_token = NULL,
227
+ activated_at = excluded.activated_at,
228
+ login_method = excluded.login_method,
229
+ conversation_id = excluded.conversation_id,
230
+ bootstrap_sent = excluded.bootstrap_sent,
231
+ bootstrap_claimed_at = NULL,
232
+ updated_at = excluded.updated_at`)
233
+ .run(input.platform, input.accountId, userId, ownerUserId, null, null, now, input.loginMethod ?? null, conversationId, conversationId ? 0 : 1, null, now);
234
+ if (conversationId) {
235
+ this.upsertConversationSummaryInDb({
236
+ platform: input.platform,
237
+ accountId: input.accountId,
238
+ conversationId,
239
+ lastSeenAt: now,
240
+ });
241
+ }
242
+ });
243
+ }
244
+ upsertConversationSummary(input) {
245
+ this.write(() => {
246
+ this.upsertConversationSummaryInDb(input);
247
+ });
248
+ }
249
+ upsertConversationDetails(input) {
250
+ this.write(() => {
251
+ const db = this.requireDb();
252
+ db.exec("BEGIN");
253
+ try {
254
+ this.upsertConversationSummaryInDb(input);
255
+ if (input.membersComplete) {
256
+ db.prepare(`DELETE FROM clawchat_conversation_members
257
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`).run(input.platform, input.accountId, input.conversationId);
258
+ for (const member of input.members ?? []) {
259
+ db.prepare(`INSERT INTO clawchat_conversation_members(
260
+ platform, account_id, conversation_id, user_id, role, raw_json, last_seen_at
261
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(input.platform, input.accountId, input.conversationId, member.userId, member.role ?? null, toJson(member.raw), member.lastSeenAt ?? null);
262
+ }
263
+ }
264
+ db.exec("COMMIT");
265
+ }
266
+ catch (err) {
267
+ db.exec("ROLLBACK");
268
+ throw err;
269
+ }
270
+ });
271
+ }
272
+ deleteConversationCache(input) {
273
+ this.write(() => {
274
+ const db = this.requireDb();
275
+ db.prepare(`DELETE FROM clawchat_conversation_members
276
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`).run(input.platform, input.accountId, input.conversationId);
277
+ db.prepare(`DELETE FROM clawchat_conversations
278
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`).run(input.platform, input.accountId, input.conversationId);
279
+ });
280
+ }
281
+ listCachedConversationIds(input) {
282
+ const limit = Math.min(100, Math.max(0, Math.trunc(input.limit ?? 50)));
283
+ return this.read(() => this.requireDb()
284
+ .prepare(`SELECT conversation_id FROM clawchat_conversations
285
+ WHERE platform = ? AND account_id = ?
286
+ ORDER BY last_seen_at DESC, conversation_id ASC
287
+ LIMIT ?`)
288
+ .all(input.platform, input.accountId, limit)
289
+ .map((row) => String(row.conversation_id))) ?? [];
290
+ }
291
+ getCachedConversation(input) {
292
+ return this.read(() => {
293
+ const row = this.requireDb()
294
+ .prepare(`SELECT conversation_id, conversation_type, metadata_version, last_seen_at, last_refreshed_at
295
+ FROM clawchat_conversations
296
+ WHERE platform = ? AND account_id = ? AND conversation_id = ?`)
297
+ .get(input.platform, input.accountId, input.conversationId);
298
+ if (typeof row?.conversation_id !== "string")
299
+ return null;
300
+ return {
301
+ conversationId: row.conversation_id,
302
+ conversationType: typeof row.conversation_type === "string" ? row.conversation_type : null,
303
+ metadataVersion: typeof row.metadata_version === "number" ? row.metadata_version : null,
304
+ lastSeenAt: typeof row.last_seen_at === "number" ? row.last_seen_at : null,
305
+ lastRefreshedAt: typeof row.last_refreshed_at === "number" ? row.last_refreshed_at : null,
306
+ };
307
+ }) ?? null;
308
+ }
309
+ getActivationConversation(input) {
310
+ return this.read(() => {
311
+ const row = this.requireDb()
312
+ .prepare(`SELECT
313
+ a.conversation_id,
314
+ c.conversation_type,
315
+ c.metadata_version,
316
+ c.last_seen_at,
317
+ c.last_refreshed_at
318
+ FROM activations a
319
+ LEFT JOIN clawchat_conversations c
320
+ ON c.platform = a.platform
321
+ AND c.account_id = a.account_id
322
+ AND c.conversation_id = a.conversation_id
323
+ WHERE a.platform = ?
324
+ AND a.account_id = ?
325
+ AND a.conversation_id IS NOT NULL
326
+ AND a.conversation_id <> ''`)
327
+ .get(input.platform, input.accountId);
328
+ if (typeof row?.conversation_id !== "string")
329
+ return null;
330
+ return {
331
+ conversationId: row.conversation_id,
332
+ conversationType: typeof row.conversation_type === "string" ? row.conversation_type : null,
333
+ metadataVersion: typeof row.metadata_version === "number" ? row.metadata_version : null,
334
+ lastSeenAt: typeof row.last_seen_at === "number" ? row.last_seen_at : null,
335
+ lastRefreshedAt: typeof row.last_refreshed_at === "number" ? row.last_refreshed_at : null,
336
+ };
337
+ }) ?? null;
338
+ }
339
+ claimPendingActivationBootstrap(input) {
340
+ return this.write(() => {
341
+ const now = Date.now();
342
+ const staleMs = Math.max(0, input.staleClaimMs ?? DEFAULT_BOOTSTRAP_CLAIM_STALE_MS);
343
+ const staleBefore = now - staleMs;
344
+ const row = this.requireDb()
345
+ .prepare(`UPDATE activations SET
346
+ bootstrap_claimed_at = ?, updated_at = ?
347
+ WHERE platform = ?
348
+ AND account_id = ?
349
+ AND conversation_id IS NOT NULL
350
+ AND conversation_id <> ''
351
+ AND bootstrap_sent = 0
352
+ AND (bootstrap_claimed_at IS NULL OR bootstrap_claimed_at <= ?)
353
+ RETURNING conversation_id`)
354
+ .get(now, now, input.platform, input.accountId, staleBefore);
355
+ const conversationId = row?.conversation_id;
356
+ return typeof conversationId === "string" && conversationId.length > 0
357
+ ? { conversationId }
358
+ : null;
359
+ }) ?? null;
360
+ }
361
+ releaseActivationBootstrapClaim(input) {
362
+ return this.write(() => {
363
+ const now = Date.now();
364
+ const result = this.requireDb()
365
+ .prepare(`UPDATE activations SET
366
+ bootstrap_claimed_at = NULL,
367
+ updated_at = ?
368
+ WHERE platform = ?
369
+ AND account_id = ?
370
+ AND conversation_id = ?
371
+ AND bootstrap_sent = 0
372
+ AND bootstrap_claimed_at IS NOT NULL`)
373
+ .run(now, input.platform, input.accountId, input.conversationId);
374
+ return result.changes > 0;
375
+ });
376
+ }
377
+ markActivationBootstrapSent(input) {
378
+ return this.write(() => {
379
+ const now = Date.now();
380
+ const result = this.requireDb()
381
+ .prepare(`UPDATE activations SET
382
+ bootstrap_sent = 1,
383
+ bootstrap_claimed_at = NULL,
384
+ updated_at = ?
385
+ WHERE platform = ?
386
+ AND account_id = ?
387
+ AND conversation_id = ?
388
+ AND bootstrap_sent = 0
389
+ AND bootstrap_claimed_at IS NOT NULL`)
390
+ .run(now, input.platform, input.accountId, input.conversationId);
391
+ return result.changes > 0;
392
+ });
393
+ }
394
+ insertMessage(input) {
395
+ return this.write(() => {
396
+ this.requireDb()
397
+ .prepare(`INSERT INTO clawchat_messages(
398
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
399
+ message_id, text, raw_json, created_at
400
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
401
+ .run(input.platform, input.accountId, input.kind, input.direction, input.eventType, input.traceId ?? null, input.chatId ?? null, input.messageId ?? null, input.text ?? null, toJson(input.raw), input.createdAt ?? Date.now());
402
+ return true;
403
+ });
404
+ }
405
+ claimMessageOnce(input) {
406
+ return this.write(() => {
407
+ const result = this.requireDb()
408
+ .prepare(`INSERT OR IGNORE INTO clawchat_messages(
409
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
410
+ message_id, text, raw_json, created_at
411
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
412
+ .run(input.platform, input.accountId, input.kind, input.direction, input.eventType, input.traceId ?? null, input.chatId ?? null, input.messageId ?? null, input.text ?? null, toJson(input.raw), input.createdAt ?? Date.now());
413
+ return result.changes > 0;
414
+ });
415
+ }
416
+ updateMessageByIdentity(input) {
417
+ this.write(() => {
418
+ this.requireDb()
419
+ .prepare(`UPDATE clawchat_messages SET
420
+ event_type = ?, trace_id = ?, chat_id = ?, text = ?, raw_json = ?
421
+ WHERE account_id = ? AND kind = ? AND direction = ? AND message_id = ?`)
422
+ .run(input.eventType, input.traceId ?? null, input.chatId ?? null, input.text ?? null, toJson(input.raw), input.accountId, input.kind, input.direction, input.messageId);
423
+ });
424
+ }
425
+ startConnection(input) {
426
+ return this.write(() => {
427
+ const now = input.connectStartedAt ?? Date.now();
428
+ const result = this.requireDb()
429
+ .prepare(`INSERT INTO connections(
430
+ platform, account_id, attempt, reconnect_count, state,
431
+ connect_started_at, created_at, updated_at
432
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
433
+ .run(input.platform, input.accountId, input.attempt ?? null, input.reconnectCount ?? null, "connecting", now, now, now);
434
+ return Number(result.lastInsertRowid);
435
+ }) ?? null;
436
+ }
437
+ markConnectSent(connectionId, options = {}) {
438
+ if (typeof connectionId !== "number")
439
+ return;
440
+ this.updateConnectionTime(connectionId, "connect_sent_at", options.at ?? Date.now());
441
+ }
442
+ markConnectionReady(connectionId, options = {}) {
443
+ if (typeof connectionId !== "number")
444
+ return;
445
+ this.write(() => {
446
+ const now = options.at ?? Date.now();
447
+ this.requireDb()
448
+ .prepare(`UPDATE connections SET
449
+ state = ?,
450
+ ready_at = ?,
451
+ resolved_device_id = CASE WHEN ? THEN ? ELSE resolved_device_id END,
452
+ delivery_mode = CASE WHEN ? THEN ? ELSE delivery_mode END,
453
+ updated_at = ?
454
+ WHERE id = ?`)
455
+ .run("ready", now, hasOwn(options, "resolvedDeviceId"), options.resolvedDeviceId ?? null, hasOwn(options, "deliveryMode"), options.deliveryMode ?? null, now, connectionId);
456
+ });
457
+ }
458
+ finishConnection(connectionId, input) {
459
+ if (typeof connectionId !== "number")
460
+ return;
461
+ this.write(() => {
462
+ const now = input.disconnectedAt ?? Date.now();
463
+ this.requireDb()
464
+ .prepare(`UPDATE connections SET
465
+ state = ?, disconnected_at = ?, close_code = ?, close_reason = ?, error = ?, updated_at = ?
466
+ WHERE id = ?`)
467
+ .run(input.state, now, input.closeCode ?? null, input.closeReason ?? null, input.error ?? null, now, connectionId);
468
+ });
469
+ }
470
+ recordToolCall(input) {
471
+ this.write(() => {
472
+ const startedAt = input.startedAt ?? Date.now();
473
+ const endedAt = input.endedAt ?? null;
474
+ const durationMs = endedAt == null ? null : Math.max(0, endedAt - startedAt);
475
+ this.requireDb()
476
+ .prepare(`INSERT INTO tool_calls(
477
+ platform, account_id, tool_name, args_json, result_json, error,
478
+ started_at, ended_at, duration_ms, created_at
479
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
480
+ .run(input.platform, input.accountId ?? null, input.toolName, toJson(input.args), toJson(input.result), toNullableString(input.error), startedAt, endedAt, durationMs, startedAt);
481
+ });
482
+ }
483
+ getActivationForTest(platform, accountId) {
484
+ return this.read(() => this.requireDb()
485
+ .prepare("SELECT * FROM activations WHERE platform = ? AND account_id = ?")
486
+ .get(platform, accountId)) ?? null;
487
+ }
488
+ listMessagesForTest() {
489
+ return this.read(() => this.requireDb()
490
+ .prepare("SELECT * FROM clawchat_messages ORDER BY id")
491
+ .all()) ?? [];
492
+ }
493
+ listConnectionsForTest() {
494
+ return this.read(() => this.requireDb().prepare("SELECT * FROM connections ORDER BY id").all()) ?? [];
495
+ }
496
+ listToolCallsForTest() {
497
+ return this.read(() => this.requireDb().prepare("SELECT * FROM tool_calls ORDER BY id").all()) ?? [];
498
+ }
499
+ listConversationCacheForTest(table) {
500
+ if (![
501
+ "clawchat_conversations",
502
+ "clawchat_conversation_members",
503
+ ].includes(table)) {
504
+ throw new Error(`unsupported conversation cache table: ${table}`);
505
+ }
506
+ return this.read(() => this.requireDb().prepare(`SELECT * FROM ${table} ORDER BY rowid`).all()) ?? [];
507
+ }
508
+ close() {
509
+ this.db?.close();
510
+ this.db = null;
511
+ this.initialized = false;
512
+ }
513
+ updateConnectionTime(connectionId, field, at) {
514
+ this.write(() => {
515
+ this.requireDb()
516
+ .prepare(`UPDATE connections SET ${field} = ?, updated_at = ? WHERE id = ?`)
517
+ .run(at, at, connectionId);
518
+ });
519
+ }
520
+ upsertConversationSummaryInDb(input) {
521
+ this.requireDb()
522
+ .prepare(`INSERT INTO clawchat_conversations(
523
+ platform, account_id, conversation_id, conversation_type, metadata_version,
524
+ last_seen_at, last_refreshed_at, raw_json
525
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
526
+ ON CONFLICT(platform, account_id, conversation_id) DO UPDATE SET
527
+ conversation_type = CASE WHEN ? THEN excluded.conversation_type ELSE clawchat_conversations.conversation_type END,
528
+ metadata_version = CASE WHEN ? THEN excluded.metadata_version ELSE clawchat_conversations.metadata_version END,
529
+ last_seen_at = CASE WHEN ? THEN excluded.last_seen_at ELSE clawchat_conversations.last_seen_at END,
530
+ last_refreshed_at = CASE WHEN ? THEN excluded.last_refreshed_at ELSE clawchat_conversations.last_refreshed_at END,
531
+ raw_json = CASE WHEN ? THEN excluded.raw_json ELSE clawchat_conversations.raw_json END`)
532
+ .run(input.platform, input.accountId, input.conversationId, input.conversationType ?? null, input.metadataVersion ?? null, input.lastSeenAt ?? null, input.lastRefreshedAt ?? null, toCacheJson(input.raw), hasOwn(input, "conversationType"), hasOwn(input, "metadataVersion"), hasOwn(input, "lastSeenAt"), hasOwn(input, "lastRefreshedAt"), hasOwn(input, "raw"));
533
+ }
534
+ read(fn) {
535
+ if (!this.ensureInitialized())
536
+ return null;
537
+ try {
538
+ return fn();
539
+ }
540
+ catch (err) {
541
+ this.logFailure("openclaw-clawchat sqlite read failed", err);
542
+ return null;
543
+ }
544
+ }
545
+ write(fn) {
546
+ if (!this.ensureInitialized())
547
+ return null;
548
+ try {
549
+ return fn();
550
+ }
551
+ catch (err) {
552
+ this.logFailure("openclaw-clawchat sqlite write failed", err);
553
+ return null;
554
+ }
555
+ }
556
+ ensureInitialized() {
557
+ if (this.disabled)
558
+ return false;
559
+ if (this.initialized)
560
+ return true;
561
+ try {
562
+ fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
563
+ this.db = new DatabaseSync(this.dbPath);
564
+ this.db.exec("PRAGMA journal_mode=WAL");
565
+ try {
566
+ fs.chmodSync(this.dbPath, 0o600);
567
+ }
568
+ catch {
569
+ // Best effort only; permissions vary by platform/filesystem.
570
+ }
571
+ this.applyMigrations();
572
+ this.initialized = true;
573
+ return true;
574
+ }
575
+ catch (err) {
576
+ this.disabled = true;
577
+ this.logFailure("openclaw-clawchat sqlite disabled after initialization failure", err);
578
+ try {
579
+ this.db?.close();
580
+ }
581
+ catch {
582
+ // best effort
583
+ }
584
+ this.db = null;
585
+ return false;
586
+ }
587
+ }
588
+ applyMigrations() {
589
+ const db = this.requireDb();
590
+ db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
591
+ version INTEGER PRIMARY KEY,
592
+ name TEXT NOT NULL,
593
+ applied_at INTEGER NOT NULL
594
+ )`);
595
+ const hasMigration = db.prepare("SELECT 1 FROM schema_migrations WHERE version = ?");
596
+ const insertMigration = db.prepare("INSERT INTO schema_migrations(version, name, applied_at) VALUES (?, ?, ?)");
597
+ for (const migration of MIGRATIONS) {
598
+ if (hasMigration.get(migration.version))
599
+ continue;
600
+ db.exec("BEGIN");
601
+ try {
602
+ db.exec(migration.sql);
603
+ insertMigration.run(migration.version, migration.name, Date.now());
604
+ db.exec("COMMIT");
605
+ }
606
+ catch (err) {
607
+ db.exec("ROLLBACK");
608
+ throw err;
609
+ }
610
+ }
611
+ }
612
+ requireDb() {
613
+ if (!this.db)
614
+ throw new Error("clawchat sqlite database is not open");
615
+ return this.db;
616
+ }
617
+ logFailure(prefix, err) {
618
+ this.log?.error?.(`${prefix}: ${safeErrorMessage(err)}`);
619
+ }
620
+ }
621
+ let singleton = null;
622
+ export function createClawChatStore(options = {}) {
623
+ return new ClawChatStore(options);
624
+ }
625
+ export function getClawChatStore(options = {}) {
626
+ const dbPath = options.dbPath ?? defaultDbPath();
627
+ if (!singleton || singleton.dbPath !== dbPath) {
628
+ singleton?.close();
629
+ singleton = createClawChatStore({ ...options, dbPath });
630
+ }
631
+ return singleton;
632
+ }
633
+ export function resetClawChatStoreForTest() {
634
+ singleton?.close();
635
+ singleton = null;
636
+ }