@clawling/clawchat-plugin-openclaw 2026.5.12-28

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 (114) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +227 -0
  3. package/dist/index.js +20 -0
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +263 -0
  6. package/dist/src/api-types.js +17 -0
  7. package/dist/src/api-types.test-d.js +10 -0
  8. package/dist/src/buffered-stream.js +177 -0
  9. package/dist/src/channel.js +66 -0
  10. package/dist/src/channel.setup.js +119 -0
  11. package/dist/src/clawchat-memory.js +403 -0
  12. package/dist/src/clawchat-metadata.js +310 -0
  13. package/dist/src/client.js +35 -0
  14. package/dist/src/commands.js +35 -0
  15. package/dist/src/config.js +274 -0
  16. package/dist/src/group-message-coalescer.js +119 -0
  17. package/dist/src/inbound.js +170 -0
  18. package/dist/src/llm-context-debug.js +86 -0
  19. package/dist/src/login.runtime.js +204 -0
  20. package/dist/src/media-runtime.js +85 -0
  21. package/dist/src/message-mapper.js +146 -0
  22. package/dist/src/mock-transport.js +31 -0
  23. package/dist/src/outbound.js +628 -0
  24. package/dist/src/plugin-prompts.js +89 -0
  25. package/dist/src/profile-prompt.js +269 -0
  26. package/dist/src/profile-sync.js +110 -0
  27. package/dist/src/prompt-injection.js +25 -0
  28. package/dist/src/protocol-types.js +63 -0
  29. package/dist/src/protocol-types.typecheck.js +1 -0
  30. package/dist/src/protocol.js +33 -0
  31. package/dist/src/reply-dispatcher.js +422 -0
  32. package/dist/src/runtime.js +1254 -0
  33. package/dist/src/storage.js +525 -0
  34. package/dist/src/streaming.js +65 -0
  35. package/dist/src/terminal-send.js +36 -0
  36. package/dist/src/tools-schema.js +208 -0
  37. package/dist/src/tools.js +920 -0
  38. package/dist/src/ws-alignment.js +178 -0
  39. package/dist/src/ws-client.js +588 -0
  40. package/dist/src/ws-log.js +19 -0
  41. package/index.ts +24 -0
  42. package/openclaw.plugin.json +169 -0
  43. package/package.json +80 -0
  44. package/prompts/default-group-bio.md +19 -0
  45. package/prompts/default-owner-behavior.md +27 -0
  46. package/prompts/platform.md +13 -0
  47. package/setup-entry.ts +4 -0
  48. package/skills/clawchat/SKILL.md +91 -0
  49. package/src/api-client.test.ts +827 -0
  50. package/src/api-client.ts +414 -0
  51. package/src/api-types.ts +146 -0
  52. package/src/channel.outbound.test.ts +433 -0
  53. package/src/channel.setup.ts +145 -0
  54. package/src/channel.test.ts +262 -0
  55. package/src/channel.ts +81 -0
  56. package/src/clawchat-memory.test.ts +480 -0
  57. package/src/clawchat-memory.ts +533 -0
  58. package/src/clawchat-metadata.test.ts +477 -0
  59. package/src/clawchat-metadata.ts +429 -0
  60. package/src/client.test.ts +169 -0
  61. package/src/client.ts +56 -0
  62. package/src/commands.test.ts +39 -0
  63. package/src/commands.ts +41 -0
  64. package/src/config.test.ts +344 -0
  65. package/src/config.ts +404 -0
  66. package/src/group-message-coalescer.test.ts +237 -0
  67. package/src/group-message-coalescer.ts +171 -0
  68. package/src/inbound.test.ts +508 -0
  69. package/src/inbound.ts +278 -0
  70. package/src/llm-context-debug.test.ts +55 -0
  71. package/src/llm-context-debug.ts +139 -0
  72. package/src/login.runtime.test.ts +737 -0
  73. package/src/login.runtime.ts +277 -0
  74. package/src/manifest.test.ts +352 -0
  75. package/src/media-runtime.test.ts +207 -0
  76. package/src/media-runtime.ts +152 -0
  77. package/src/message-mapper.test.ts +201 -0
  78. package/src/message-mapper.ts +174 -0
  79. package/src/mock-transport.test.ts +35 -0
  80. package/src/mock-transport.ts +38 -0
  81. package/src/outbound.test.ts +1269 -0
  82. package/src/outbound.ts +803 -0
  83. package/src/plugin-entry.test.ts +38 -0
  84. package/src/plugin-prompts.test.ts +94 -0
  85. package/src/plugin-prompts.ts +107 -0
  86. package/src/profile-prompt.test.ts +274 -0
  87. package/src/profile-prompt.ts +351 -0
  88. package/src/profile-sync.test.ts +539 -0
  89. package/src/profile-sync.ts +191 -0
  90. package/src/prompt-injection.test.ts +39 -0
  91. package/src/prompt-injection.ts +45 -0
  92. package/src/protocol-types.test.ts +69 -0
  93. package/src/protocol-types.ts +296 -0
  94. package/src/protocol-types.typecheck.ts +89 -0
  95. package/src/protocol.test.ts +39 -0
  96. package/src/protocol.ts +42 -0
  97. package/src/reply-dispatcher.test.ts +1324 -0
  98. package/src/reply-dispatcher.ts +555 -0
  99. package/src/runtime.test.ts +4719 -0
  100. package/src/runtime.ts +1493 -0
  101. package/src/scripts.test.ts +85 -0
  102. package/src/storage.test.ts +560 -0
  103. package/src/storage.ts +807 -0
  104. package/src/terminal-send.test.ts +81 -0
  105. package/src/terminal-send.ts +56 -0
  106. package/src/tools-schema.ts +337 -0
  107. package/src/tools.test.ts +933 -0
  108. package/src/tools.ts +1185 -0
  109. package/src/ws-alignment.test.ts +103 -0
  110. package/src/ws-alignment.ts +275 -0
  111. package/src/ws-client.test.ts +1217 -0
  112. package/src/ws-client.ts +662 -0
  113. package/src/ws-log.test.ts +32 -0
  114. package/src/ws-log.ts +31 -0
@@ -0,0 +1,525 @@
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
+ `,
105
+ },
106
+ {
107
+ version: 4,
108
+ name: "activation_owner_user_id",
109
+ sql: `
110
+ ALTER TABLE activations ADD COLUMN owner_user_id TEXT;
111
+ `,
112
+ },
113
+ {
114
+ version: 5,
115
+ name: "connection_metadata",
116
+ sql: `
117
+ ALTER TABLE connections ADD COLUMN resolved_device_id TEXT;
118
+ ALTER TABLE connections ADD COLUMN delivery_mode TEXT;
119
+ `,
120
+ },
121
+ {
122
+ version: 6,
123
+ name: "message_ack_status",
124
+ sql: `
125
+ ALTER TABLE clawchat_messages ADD COLUMN send_status TEXT;
126
+ ALTER TABLE clawchat_messages ADD COLUMN protocol_message_id TEXT;
127
+ ALTER TABLE clawchat_messages ADD COLUMN acked_at INTEGER;
128
+ ALTER TABLE clawchat_messages ADD COLUMN send_error TEXT;
129
+ `,
130
+ },
131
+ ];
132
+ function fallbackDbPath() {
133
+ const home = process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw");
134
+ return path.join(home, DB_FILENAME);
135
+ }
136
+ export function clawChatDbPathForStateDir(stateDir) {
137
+ return path.join(stateDir, DB_FILENAME);
138
+ }
139
+ export function defaultDbPath() {
140
+ try {
141
+ return clawChatDbPathForStateDir(resolveStateDir());
142
+ }
143
+ catch {
144
+ return fallbackDbPath();
145
+ }
146
+ }
147
+ function toNullableString(value) {
148
+ return typeof value === "string" ? value : value == null ? null : String(value);
149
+ }
150
+ function toJson(value) {
151
+ if (value === undefined)
152
+ return null;
153
+ try {
154
+ return JSON.stringify(value);
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ }
160
+ function hasOwn(value, key) {
161
+ return Object.prototype.hasOwnProperty.call(value, key) ? 1 : 0;
162
+ }
163
+ function safeErrorMessage(err) {
164
+ const message = err instanceof Error ? err.message : String(err);
165
+ return message
166
+ .replace(/(access[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
167
+ .replace(/(refresh[_-]?token["'\s:=]+)[^"'\s,}]+/gi, "$1[REDACTED]")
168
+ .replace(/(authorization["'\s:=]+bearer\s+)[^"'\s,}]+/gi, "$1[REDACTED]");
169
+ }
170
+ export class ClawChatStore {
171
+ dbPath;
172
+ log;
173
+ db = null;
174
+ initialized = false;
175
+ disabled = false;
176
+ constructor(options = {}) {
177
+ this.dbPath = options.dbPath ?? defaultDbPath();
178
+ this.log = options.log;
179
+ }
180
+ initialize() {
181
+ this.ensureInitialized();
182
+ }
183
+ listAppliedMigrations() {
184
+ return this.read(() => this.requireDb()
185
+ .prepare("SELECT version, name FROM schema_migrations ORDER BY version")
186
+ .all()) ?? [];
187
+ }
188
+ upsertActivation(input) {
189
+ this.write(() => {
190
+ const now = input.activatedAt ?? Date.now();
191
+ const conversationId = input.conversationId?.trim() || null;
192
+ const userId = input.userId?.trim() || null;
193
+ const ownerUserId = input.ownerUserId?.trim() || null;
194
+ const accessToken = input.accessToken?.trim() || null;
195
+ const refreshToken = input.refreshToken?.trim() || null;
196
+ this.requireDb()
197
+ .prepare(`INSERT INTO activations(
198
+ platform, account_id, user_id, owner_user_id, access_token, refresh_token,
199
+ activated_at, login_method, conversation_id, bootstrap_sent,
200
+ bootstrap_claimed_at, updated_at
201
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
202
+ ON CONFLICT(platform, account_id) DO UPDATE SET
203
+ user_id = excluded.user_id,
204
+ owner_user_id = excluded.owner_user_id,
205
+ access_token = excluded.access_token,
206
+ refresh_token = excluded.refresh_token,
207
+ activated_at = excluded.activated_at,
208
+ login_method = excluded.login_method,
209
+ conversation_id = excluded.conversation_id,
210
+ bootstrap_sent = excluded.bootstrap_sent,
211
+ bootstrap_claimed_at = NULL,
212
+ updated_at = excluded.updated_at`)
213
+ .run(input.platform, input.accountId, userId, ownerUserId, accessToken, refreshToken, now, input.loginMethod ?? null, conversationId, conversationId ? 0 : 1, null, now);
214
+ void conversationId;
215
+ });
216
+ }
217
+ getActivationConversation(input) {
218
+ return this.read(() => {
219
+ const row = this.requireDb()
220
+ .prepare(`SELECT conversation_id
221
+ FROM activations
222
+ WHERE platform = ?
223
+ AND account_id = ?
224
+ AND conversation_id IS NOT NULL
225
+ AND conversation_id <> ''`)
226
+ .get(input.platform, input.accountId);
227
+ if (typeof row?.conversation_id !== "string")
228
+ return null;
229
+ return {
230
+ conversationId: row.conversation_id,
231
+ conversationType: null,
232
+ metadataVersion: null,
233
+ lastSeenAt: null,
234
+ lastRefreshedAt: null,
235
+ };
236
+ }) ?? null;
237
+ }
238
+ claimPendingActivationBootstrap(input) {
239
+ return this.write(() => {
240
+ const now = Date.now();
241
+ const staleMs = Math.max(0, input.staleClaimMs ?? DEFAULT_BOOTSTRAP_CLAIM_STALE_MS);
242
+ const staleBefore = now - staleMs;
243
+ const row = this.requireDb()
244
+ .prepare(`UPDATE activations SET
245
+ bootstrap_claimed_at = ?, updated_at = ?
246
+ WHERE platform = ?
247
+ AND account_id = ?
248
+ AND conversation_id IS NOT NULL
249
+ AND conversation_id <> ''
250
+ AND bootstrap_sent = 0
251
+ AND (bootstrap_claimed_at IS NULL OR bootstrap_claimed_at <= ?)
252
+ RETURNING conversation_id`)
253
+ .get(now, now, input.platform, input.accountId, staleBefore);
254
+ const conversationId = row?.conversation_id;
255
+ return typeof conversationId === "string" && conversationId.length > 0
256
+ ? { conversationId }
257
+ : null;
258
+ }) ?? null;
259
+ }
260
+ releaseActivationBootstrapClaim(input) {
261
+ return this.write(() => {
262
+ const now = Date.now();
263
+ const result = this.requireDb()
264
+ .prepare(`UPDATE activations SET
265
+ bootstrap_claimed_at = NULL,
266
+ updated_at = ?
267
+ WHERE platform = ?
268
+ AND account_id = ?
269
+ AND conversation_id = ?
270
+ AND bootstrap_sent = 0
271
+ AND bootstrap_claimed_at IS NOT NULL`)
272
+ .run(now, input.platform, input.accountId, input.conversationId);
273
+ return result.changes > 0;
274
+ });
275
+ }
276
+ markActivationBootstrapSent(input) {
277
+ return this.write(() => {
278
+ const now = Date.now();
279
+ const result = this.requireDb()
280
+ .prepare(`UPDATE activations SET
281
+ bootstrap_sent = 1,
282
+ bootstrap_claimed_at = NULL,
283
+ updated_at = ?
284
+ WHERE platform = ?
285
+ AND account_id = ?
286
+ AND conversation_id = ?
287
+ AND bootstrap_sent = 0
288
+ AND bootstrap_claimed_at IS NOT NULL`)
289
+ .run(now, input.platform, input.accountId, input.conversationId);
290
+ return result.changes > 0;
291
+ });
292
+ }
293
+ insertMessage(input) {
294
+ return this.write(() => {
295
+ this.requireDb()
296
+ .prepare(`INSERT INTO clawchat_messages(
297
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
298
+ message_id, text, raw_json, created_at, send_status
299
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
300
+ .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(), input.direction === "outbound" ? "pending" : null);
301
+ return true;
302
+ });
303
+ }
304
+ claimMessageOnce(input) {
305
+ return this.write(() => {
306
+ const result = this.requireDb()
307
+ .prepare(`INSERT OR IGNORE INTO clawchat_messages(
308
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
309
+ message_id, text, raw_json, created_at, send_status
310
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
311
+ .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(), input.direction === "outbound" ? "pending" : null);
312
+ return result.changes > 0;
313
+ });
314
+ }
315
+ markMessageAcknowledged(input) {
316
+ return this.write(() => {
317
+ const result = this.requireDb()
318
+ .prepare(`UPDATE clawchat_messages SET
319
+ send_status = 'acknowledged',
320
+ protocol_message_id = ?,
321
+ acked_at = ?,
322
+ send_error = NULL
323
+ WHERE account_id = ? AND kind = ? AND direction = ? AND message_id = ?`)
324
+ .run(input.protocolMessageId ?? input.messageId, input.ackedAt ?? Date.now(), input.accountId, input.kind, input.direction, input.messageId);
325
+ return result.changes > 0;
326
+ });
327
+ }
328
+ updateMessageByIdentity(input) {
329
+ this.write(() => {
330
+ this.requireDb()
331
+ .prepare(`UPDATE clawchat_messages SET
332
+ event_type = ?, trace_id = ?, chat_id = ?, text = ?, raw_json = ?
333
+ WHERE account_id = ? AND kind = ? AND direction = ? AND message_id = ?`)
334
+ .run(input.eventType, input.traceId ?? null, input.chatId ?? null, input.text ?? null, toJson(input.raw), input.accountId, input.kind, input.direction, input.messageId);
335
+ });
336
+ }
337
+ startConnection(input) {
338
+ return this.write(() => {
339
+ const now = input.connectStartedAt ?? Date.now();
340
+ const result = this.requireDb()
341
+ .prepare(`INSERT INTO connections(
342
+ platform, account_id, attempt, reconnect_count, state,
343
+ connect_started_at, created_at, updated_at
344
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
345
+ .run(input.platform, input.accountId, input.attempt ?? null, input.reconnectCount ?? null, "connecting", now, now, now);
346
+ return Number(result.lastInsertRowid);
347
+ }) ?? null;
348
+ }
349
+ markConnectSent(connectionId, options = {}) {
350
+ if (typeof connectionId !== "number")
351
+ return;
352
+ this.updateConnectionTime(connectionId, "connect_sent_at", options.at ?? Date.now());
353
+ }
354
+ markConnectionReady(connectionId, options = {}) {
355
+ if (typeof connectionId !== "number")
356
+ return;
357
+ this.write(() => {
358
+ const now = options.at ?? Date.now();
359
+ this.requireDb()
360
+ .prepare(`UPDATE connections SET
361
+ state = ?,
362
+ ready_at = ?,
363
+ resolved_device_id = CASE WHEN ? THEN ? ELSE resolved_device_id END,
364
+ delivery_mode = CASE WHEN ? THEN ? ELSE delivery_mode END,
365
+ updated_at = ?
366
+ WHERE id = ?`)
367
+ .run("ready", now, hasOwn(options, "resolvedDeviceId"), options.resolvedDeviceId ?? null, hasOwn(options, "deliveryMode"), options.deliveryMode ?? null, now, connectionId);
368
+ });
369
+ }
370
+ finishConnection(connectionId, input) {
371
+ if (typeof connectionId !== "number")
372
+ return;
373
+ this.write(() => {
374
+ const now = input.disconnectedAt ?? Date.now();
375
+ this.requireDb()
376
+ .prepare(`UPDATE connections SET
377
+ state = ?, disconnected_at = ?, close_code = ?, close_reason = ?, error = ?, updated_at = ?
378
+ WHERE id = ?`)
379
+ .run(input.state, now, input.closeCode ?? null, input.closeReason ?? null, input.error ?? null, now, connectionId);
380
+ });
381
+ }
382
+ recordToolCall(input) {
383
+ this.write(() => {
384
+ const startedAt = input.startedAt ?? Date.now();
385
+ const endedAt = input.endedAt ?? null;
386
+ const durationMs = endedAt == null ? null : Math.max(0, endedAt - startedAt);
387
+ this.requireDb()
388
+ .prepare(`INSERT INTO tool_calls(
389
+ platform, account_id, tool_name, args_json, result_json, error,
390
+ started_at, ended_at, duration_ms, created_at
391
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
392
+ .run(input.platform, input.accountId ?? null, input.toolName, toJson(input.args), toJson(input.result), toNullableString(input.error), startedAt, endedAt, durationMs, startedAt);
393
+ });
394
+ }
395
+ getActivationForTest(platform, accountId) {
396
+ return this.read(() => this.requireDb()
397
+ .prepare("SELECT * FROM activations WHERE platform = ? AND account_id = ?")
398
+ .get(platform, accountId)) ?? null;
399
+ }
400
+ listMessagesForTest() {
401
+ return this.read(() => this.requireDb()
402
+ .prepare("SELECT * FROM clawchat_messages ORDER BY id")
403
+ .all()) ?? [];
404
+ }
405
+ listConnectionsForTest() {
406
+ return this.read(() => this.requireDb().prepare("SELECT * FROM connections ORDER BY id").all()) ?? [];
407
+ }
408
+ listToolCallsForTest() {
409
+ return this.read(() => this.requireDb().prepare("SELECT * FROM tool_calls ORDER BY id").all()) ?? [];
410
+ }
411
+ close() {
412
+ this.db?.close();
413
+ this.db = null;
414
+ this.initialized = false;
415
+ }
416
+ updateConnectionTime(connectionId, field, at) {
417
+ this.write(() => {
418
+ this.requireDb()
419
+ .prepare(`UPDATE connections SET ${field} = ?, updated_at = ? WHERE id = ?`)
420
+ .run(at, at, connectionId);
421
+ });
422
+ }
423
+ read(fn) {
424
+ if (!this.ensureInitialized())
425
+ return null;
426
+ try {
427
+ return fn();
428
+ }
429
+ catch (err) {
430
+ this.logFailure("clawchat-plugin-openclaw sqlite read failed", err);
431
+ return null;
432
+ }
433
+ }
434
+ write(fn) {
435
+ if (!this.ensureInitialized())
436
+ return null;
437
+ try {
438
+ return fn();
439
+ }
440
+ catch (err) {
441
+ this.logFailure("clawchat-plugin-openclaw sqlite write failed", err);
442
+ return null;
443
+ }
444
+ }
445
+ ensureInitialized() {
446
+ if (this.disabled)
447
+ return false;
448
+ if (this.initialized)
449
+ return true;
450
+ try {
451
+ fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
452
+ this.db = new DatabaseSync(this.dbPath);
453
+ this.db.exec("PRAGMA journal_mode=WAL");
454
+ try {
455
+ fs.chmodSync(this.dbPath, 0o600);
456
+ }
457
+ catch {
458
+ // Best effort only; permissions vary by platform/filesystem.
459
+ }
460
+ this.applyMigrations();
461
+ this.initialized = true;
462
+ return true;
463
+ }
464
+ catch (err) {
465
+ this.disabled = true;
466
+ this.logFailure("clawchat-plugin-openclaw sqlite disabled after initialization failure", err);
467
+ try {
468
+ this.db?.close();
469
+ }
470
+ catch {
471
+ // best effort
472
+ }
473
+ this.db = null;
474
+ return false;
475
+ }
476
+ }
477
+ applyMigrations() {
478
+ const db = this.requireDb();
479
+ db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
480
+ version INTEGER PRIMARY KEY,
481
+ name TEXT NOT NULL,
482
+ applied_at INTEGER NOT NULL
483
+ )`);
484
+ const hasMigration = db.prepare("SELECT 1 FROM schema_migrations WHERE version = ?");
485
+ const insertMigration = db.prepare("INSERT INTO schema_migrations(version, name, applied_at) VALUES (?, ?, ?)");
486
+ for (const migration of MIGRATIONS) {
487
+ if (hasMigration.get(migration.version))
488
+ continue;
489
+ db.exec("BEGIN");
490
+ try {
491
+ db.exec(migration.sql);
492
+ insertMigration.run(migration.version, migration.name, Date.now());
493
+ db.exec("COMMIT");
494
+ }
495
+ catch (err) {
496
+ db.exec("ROLLBACK");
497
+ throw err;
498
+ }
499
+ }
500
+ }
501
+ requireDb() {
502
+ if (!this.db)
503
+ throw new Error("clawchat sqlite database is not open");
504
+ return this.db;
505
+ }
506
+ logFailure(prefix, err) {
507
+ this.log?.error?.(`${prefix}: ${safeErrorMessage(err)}`);
508
+ }
509
+ }
510
+ let singleton = null;
511
+ export function createClawChatStore(options = {}) {
512
+ return new ClawChatStore(options);
513
+ }
514
+ export function getClawChatStore(options = {}) {
515
+ const dbPath = options.dbPath ?? defaultDbPath();
516
+ if (!singleton || singleton.dbPath !== dbPath) {
517
+ singleton?.close();
518
+ singleton = createClawChatStore({ ...options, dbPath });
519
+ }
520
+ return singleton;
521
+ }
522
+ export function resetClawChatStoreForTest() {
523
+ singleton?.close();
524
+ singleton = null;
525
+ }
@@ -0,0 +1,65 @@
1
+ import { emitStreamAdd, emitStreamCreated, emitStreamDone, emitStreamFailed, } from "./client.js";
2
+ function resolveRouting(params) {
3
+ if (params.routing)
4
+ return params.routing;
5
+ if (params.to)
6
+ return { chatId: params.to.id, chatType: params.to.type };
7
+ throw new Error("openclaw-clawchat streaming requires routing");
8
+ }
9
+ /**
10
+ * Emit one full streaming lifecycle for a pre-chunked reply.
11
+ *
12
+ * Sequence:
13
+ * typing(true)
14
+ * message.created (sequence 0)
15
+ * message.add (sequence 1..N, one per chunk)
16
+ * message.done (sequence N)
17
+ * typing(false)
18
+ *
19
+ * With zero chunks: typing(true) -> created -> done -> typing(false).
20
+ */
21
+ export async function sendStreamingText(params) {
22
+ const routing = resolveRouting(params);
23
+ const emitTyping = params.emitTyping !== false;
24
+ if (emitTyping) {
25
+ params.client.typing(routing.chatId, true);
26
+ }
27
+ emitStreamCreated(params.client, {
28
+ messageId: params.messageId,
29
+ routing,
30
+ });
31
+ let sequence = -1;
32
+ let fullText = "";
33
+ for (const chunk of params.chunks) {
34
+ sequence += 1;
35
+ fullText += chunk;
36
+ emitStreamAdd(params.client, {
37
+ messageId: params.messageId,
38
+ routing,
39
+ sequence,
40
+ fullText,
41
+ textDelta: chunk,
42
+ });
43
+ }
44
+ emitStreamDone(params.client, {
45
+ messageId: params.messageId,
46
+ routing,
47
+ finalSequence: Math.max(sequence, 0),
48
+ finalText: fullText,
49
+ });
50
+ if (emitTyping) {
51
+ params.client.typing(routing.chatId, false);
52
+ }
53
+ }
54
+ export async function sendStreamingFailure(params) {
55
+ const routing = resolveRouting(params);
56
+ emitStreamFailed(params.client, {
57
+ messageId: params.messageId,
58
+ routing,
59
+ sequence: params.currentSequence,
60
+ reason: params.reason,
61
+ });
62
+ if (params.emitTyping !== false) {
63
+ params.client.typing(routing.chatId, false);
64
+ }
65
+ }
@@ -0,0 +1,36 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const DEFAULT_TTL_MS = 60_000;
3
+ const terminalSends = new Map();
4
+ const terminalSendScope = new AsyncLocalStorage();
5
+ function key(accountId, chatId, scopeId) {
6
+ return `${accountId}\0${chatId}\0${scopeId}`;
7
+ }
8
+ export function runWithTerminalClawChatSendScope(scopeId, fn) {
9
+ return terminalSendScope.run(scopeId, fn);
10
+ }
11
+ export function markTerminalClawChatSend(params) {
12
+ const scopeId = params.scopeId ?? terminalSendScope.getStore();
13
+ if (!scopeId)
14
+ return;
15
+ terminalSends.set(key(params.accountId, params.chatId, scopeId), {
16
+ messageId: params.messageId,
17
+ expiresAt: (params.nowMs ?? Date.now()) + (params.ttlMs ?? DEFAULT_TTL_MS),
18
+ scopeId,
19
+ });
20
+ }
21
+ export function consumeTerminalClawChatSend(params) {
22
+ const scopeId = params.scopeId ?? terminalSendScope.getStore();
23
+ if (!scopeId)
24
+ return null;
25
+ const recordKey = key(params.accountId, params.chatId, scopeId);
26
+ const record = terminalSends.get(recordKey);
27
+ if (!record)
28
+ return null;
29
+ terminalSends.delete(recordKey);
30
+ if (record.expiresAt <= (params.nowMs ?? Date.now()))
31
+ return null;
32
+ return record;
33
+ }
34
+ export function clearTerminalClawChatSendsForTest() {
35
+ terminalSends.clear();
36
+ }