@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
@@ -1,4 +1,5 @@
1
- import type { ClawlingChatClient, Fragment } from "@newbase-clawchat/sdk";
1
+ import type { Fragment } from "./protocol-types.ts";
2
+ import type { ClawlingChatClient } from "./ws-client.ts";
2
3
  import {
3
4
  interactiveReplyToPresentation,
4
5
  renderMessagePresentationFallbackText,
@@ -16,12 +17,16 @@ import {
16
17
  mergeStreamingText,
17
18
  type BufferedStreamSession,
18
19
  } from "./buffered-stream.ts";
19
- import { emitFinalStreamReply, type ChatType, type EnvelopeRouting } from "./client.ts";
20
+ import type { ChatType, EnvelopeRouting } from "./client.ts";
20
21
  import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
21
- import { textToFragments } from "./message-mapper.ts";
22
22
  import { uploadOutboundMedia, type ClawlingMediaFragment } from "./media-runtime.ts";
23
- import { sendOpenclawClawlingText, type OutboundReplyCtx } from "./outbound.ts";
23
+ import {
24
+ sendOpenclawClawlingText,
25
+ type OutboundReplyCtx,
26
+ type SendResult,
27
+ } from "./outbound.ts";
24
28
  import { sendStreamingFailure } from "./streaming.ts";
29
+ import type { ClawChatStore } from "./storage.ts";
25
30
 
26
31
  export interface ReplyDispatcherOptions {
27
32
  cfg: OpenClawConfig;
@@ -52,6 +57,7 @@ export interface ReplyDispatcherOptions {
52
57
  senderNickName: string;
53
58
  bodyText: string;
54
59
  };
60
+ store?: Pick<ClawChatStore, "insertMessage" | "claimMessageOnce" | "updateMessageByIdentity"> | null;
55
61
  log?: { info?: (m: string) => void; error?: (m: string) => void };
56
62
  }
57
63
 
@@ -64,6 +70,12 @@ type StreamingReplyHooks = {
64
70
  onReasoningStream?: (payload: ReplyPayload) => Promise<void>;
65
71
  };
66
72
 
73
+ type ClawChatReplyOptions = TypedReplyDispatcherResult["replyOptions"] &
74
+ StreamingReplyHooks & {
75
+ sourceReplyDeliveryMode: "automatic";
76
+ disableBlockStreaming?: boolean;
77
+ };
78
+
67
79
  type RichAction = {
68
80
  id: string;
69
81
  label: string;
@@ -195,7 +207,7 @@ function resolvePayloadText(payload: ReplyPayload): string {
195
207
  */
196
208
  export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOptions): {
197
209
  dispatcher: TypedReplyDispatcherResult["dispatcher"];
198
- replyOptions: TypedReplyDispatcherResult["replyOptions"] & StreamingReplyHooks;
210
+ replyOptions: ClawChatReplyOptions;
199
211
  markDispatchIdle: TypedReplyDispatcherResult["markDispatchIdle"];
200
212
  } {
201
213
  const {
@@ -207,6 +219,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
207
219
  replyCtx,
208
220
  inboundMessageId,
209
221
  inboundForFinalReply,
222
+ store,
210
223
  log,
211
224
  } = options;
212
225
  const routing: EnvelopeRouting = { chatId: target.chatId, chatType: target.chatType };
@@ -246,18 +259,94 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
246
259
  let streamingClosed = false;
247
260
  let runFailed = false;
248
261
  let runDone = false;
262
+ let streamClaimAttempted = false;
249
263
  // `streamCreatedEmitted` is the authoritative guard: once a `message.created`
250
264
  // has been emitted for this dispatcher instance, never emit another — even
251
265
  // if `onReplyStart` fires again or a pre-onReplyStart `onPartialReply`
252
266
  // raced the lazy open path.
253
267
  let streamCreatedEmitted = false;
254
268
 
269
+ const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
270
+ const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
271
+ const claimOutbound = (
272
+ eventType: "message.created" | "message.send" | "message.reply",
273
+ messageId: string,
274
+ text: string,
275
+ raw: unknown,
276
+ ): true | false | null => {
277
+ if (!store || !messageId) return null;
278
+ try {
279
+ return store.claimMessageOnce({
280
+ platform: "openclaw",
281
+ accountId: account.accountId,
282
+ kind: "message",
283
+ direction: "outbound",
284
+ eventType,
285
+ chatId: target.chatId,
286
+ messageId,
287
+ text,
288
+ raw,
289
+ });
290
+ } catch {
291
+ log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound claim failed`);
292
+ return null;
293
+ }
294
+ };
295
+ const updateOutbound = (
296
+ eventType: "message.reply",
297
+ messageId: string,
298
+ text: string,
299
+ raw: unknown,
300
+ ) => {
301
+ if (!store || !messageId) return;
302
+ try {
303
+ store.updateMessageByIdentity({
304
+ accountId: account.accountId,
305
+ kind: "message",
306
+ direction: "outbound",
307
+ eventType,
308
+ chatId: target.chatId,
309
+ messageId,
310
+ text,
311
+ raw,
312
+ });
313
+ } catch {
314
+ log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound update failed; continuing`);
315
+ }
316
+ };
317
+ const recordOutbound = (kind: "message" | "thinking", messageId: string, text: string) => {
318
+ if (!store || !messageId) return;
319
+ try {
320
+ store.insertMessage({
321
+ platform: "openclaw",
322
+ accountId: account.accountId,
323
+ kind,
324
+ direction: "outbound",
325
+ eventType: outboundEventType(),
326
+ chatId: target.chatId,
327
+ messageId,
328
+ text,
329
+ raw: outboundRaw(),
330
+ });
331
+ } catch {
332
+ log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound insert failed; continuing`);
333
+ }
334
+ };
335
+ const recordThinkingIfLinked = (messageId: string) => {
336
+ const thinkingText = reasoningText.trim();
337
+ if (!thinkingText) return;
338
+ recordOutbound("thinking", messageId, thinkingText);
339
+ reasoningText = "";
340
+ };
341
+
255
342
  const mintStreamingMessageId = () =>
256
343
  `${account.userId}-stream-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
344
+ const mintStaticMessageId = () =>
345
+ `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
257
346
 
258
347
  const openSessionIfNeeded = () => {
259
- if (!streamingEnabled || streamingSession || streamCreatedEmitted) return;
260
- streamCreatedEmitted = true;
348
+ if (!streamingEnabled || streamingSession || streamCreatedEmitted || streamClaimAttempted) return;
349
+ streamClaimAttempted = true;
261
350
  // Mint a fresh agent-side message_id at `message.created` time. All
262
351
  // subsequent `message.add` / `message.done` / `message.reply` frames for
263
352
  // this stream reuse it. Once the stream finalizes (done or reply), this
@@ -266,6 +355,21 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
266
355
  // `replyTo.msgId`; keeping the two distinct avoids the agent's reply
267
356
  // frames shadowing the user turn they answer.
268
357
  streamingMessageId = mintStreamingMessageId();
358
+ const claimed = claimOutbound(
359
+ "message.created",
360
+ streamingMessageId,
361
+ "",
362
+ { target, replyCtx: replyCtx ?? null, mode: "stream" },
363
+ );
364
+ if (claimed !== true) {
365
+ streamCreatedEmitted = false;
366
+ streamingMessageId = "";
367
+ log?.[claimed === false ? "info" : "error"]?.(
368
+ `[${account.accountId}] openclaw-clawchat stream outbound skipped reason=${claimed === false ? "duplicate" : "claim_unavailable"}`,
369
+ );
370
+ return;
371
+ }
372
+ streamCreatedEmitted = true;
269
373
  streamingSession = openBufferedStreamingSession({
270
374
  client,
271
375
  routing,
@@ -318,16 +422,31 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
318
422
  text: string,
319
423
  mediaFragments: ClawlingMediaFragment[] = [],
320
424
  richFragments: Fragment[] = [],
321
- ) => {
322
- if (!text.trim() && mediaFragments.length === 0 && richFragments.length === 0) return;
425
+ options: { recordMessage?: boolean } = {},
426
+ ): Promise<SendResult | null> => {
427
+ if (!text.trim() && mediaFragments.length === 0 && richFragments.length === 0) return null;
323
428
  log?.info?.(
324
429
  `[${account.accountId}] openclaw-clawchat sending static text_len=${text.length} media=${mediaFragments.length} rich=${richFragments.length} to=${target.chatId}`,
325
430
  );
326
- await sendOpenclawClawlingText({
431
+ const messageId = mintStaticMessageId();
432
+ const raw = { target, replyCtx: replyCtx ?? null, mode: "static" };
433
+ const claimed = options.recordMessage
434
+ ? claimOutbound(outboundEventType(), messageId, text, raw)
435
+ : true;
436
+ if (claimed === false) {
437
+ log?.info?.(`[${account.accountId}] openclaw-clawchat outbound duplicate skipped msg=${messageId}`);
438
+ return null;
439
+ }
440
+ if (claimed === null) {
441
+ log?.error?.(`[${account.accountId}] openclaw-clawchat outbound skipped msg=${messageId} reason=claim_unavailable`);
442
+ return null;
443
+ }
444
+ const result = await sendOpenclawClawlingText({
327
445
  client,
328
446
  account,
329
447
  to: target,
330
448
  text,
449
+ messageId,
331
450
  ...(replyCtx ? { replyCtx } : {}),
332
451
  ...(richFragments.length > 0 ? { richFragments } : {}),
333
452
  ...(mediaFragments.length > 0 ? { mediaFragments } : {}),
@@ -336,6 +455,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
336
455
  log?.info?.(
337
456
  `[${account.accountId}] openclaw-clawchat send complete to=${target.chatId}`,
338
457
  );
458
+ return result;
339
459
  };
340
460
 
341
461
  const logDetachedFailure = (action: string, error: unknown) => {
@@ -351,6 +471,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
351
471
  const emitFinalConsolidatedMessage = async () => {
352
472
  if (finalEmitted) return;
353
473
  finalEmitted = true;
474
+ if (!streamingMessageId) return;
354
475
  const mergedMedia = await uploadMediaUrls(accumulatedMediaUrls.slice());
355
476
  const mergedText = streamText.trim();
356
477
  if (!mergedText && finalRichFragments.length === 0 && mergedMedia.length === 0) {
@@ -362,29 +483,35 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
362
483
  log?.info?.(
363
484
  `[${account.accountId}] openclaw-clawchat emitting consolidated final (message.reply) msg=${streamingMessageId} text_len=${mergedText.length} media=${mergedMedia.length}`,
364
485
  );
365
- const bodyFragments: Fragment[] = [
366
- ...(mergedText ? textToFragments(mergedText) : []),
367
- ...finalRichFragments,
368
- // mediaFragments is the local wide shape; cast at SDK boundary as
369
- // we do in outbound.ts.
370
- ...(mergedMedia as Fragment[]),
371
- ];
486
+ const finalReplyCtx: OutboundReplyCtx = {
487
+ replyToMessageId: inboundMessageId ?? streamingMessageId,
488
+ replyPreviewChatId: inboundForFinalReply?.chatId ?? target.chatId,
489
+ replyPreviewSenderId: inboundForFinalReply?.senderId ?? target.chatId,
490
+ replyPreviewNickName:
491
+ inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
492
+ replyPreviewText: inboundForFinalReply?.bodyText ?? "",
493
+ };
372
494
  // Streaming message_id must match the created/add/done frames so the
373
- // backend can correlate the consolidated reply with the stream.
374
- emitFinalStreamReply(client, {
495
+ // backend can correlate the consolidated reply with the stream. Use the
496
+ // ackable send path so disconnects queue the final user-visible answer.
497
+ await sendOpenclawClawlingText({
498
+ client,
499
+ account,
500
+ to: target,
501
+ text: mergedText,
375
502
  messageId: streamingMessageId,
376
- routing,
377
- replyTo: {
378
- msgId: inboundMessageId ?? streamingMessageId,
379
- previewId: inboundForFinalReply?.senderId ?? target.chatId,
380
- nickName:
381
- inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
382
- fragments: inboundForFinalReply?.bodyText
383
- ? [{ kind: "text", text: inboundForFinalReply.bodyText }]
384
- : [],
385
- },
386
- body: { fragments: bodyFragments },
503
+ replyCtx: finalReplyCtx,
504
+ ...(finalRichFragments.length > 0 ? { richFragments: finalRichFragments } : {}),
505
+ ...(mergedMedia.length > 0 ? { mediaFragments: mergedMedia } : {}),
506
+ log,
387
507
  });
508
+ updateOutbound(
509
+ "message.reply",
510
+ streamingMessageId,
511
+ mergedText,
512
+ { target, replyCtx: replyCtx ?? null, mode: "stream-final" },
513
+ );
514
+ recordThinkingIfLinked(streamingMessageId);
388
515
  };
389
516
 
390
517
  const ingestFinalPayload = (payload: ReplyPayload, text: string, richFragment: Fragment | null) => {
@@ -443,7 +570,9 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
443
570
  reasoningText = mergeStreamingText(reasoningText, text);
444
571
  await queueStreamSnapshot();
445
572
  } else {
446
- await sendStatic(text);
573
+ reasoningText = mergeStreamingText(reasoningText, text);
574
+ const result = await sendStatic(text, [], [], { recordMessage: true });
575
+ if (result?.messageId) recordThinkingIfLinked(result.messageId);
447
576
  }
448
577
  return;
449
578
  }
@@ -458,13 +587,21 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
458
587
  );
459
588
  // For streaming: consolidated final is emitted in onIdle after done.
460
589
  // For static: emit immediately.
461
- if (!streamingEnabled) {
590
+ if (streamingEnabled) {
591
+ if (text.trim()) {
592
+ await queueStreamSnapshot();
593
+ } else if (richFragment || urls.length > 0) {
594
+ openSessionIfNeeded();
595
+ }
596
+ } else {
462
597
  const mediaFragments = await uploadMediaUrls(urls);
463
- await sendStatic(
598
+ const result = await sendStatic(
464
599
  text,
465
600
  mediaFragments,
466
601
  richFragment && account.richInteractions ? ([richFragment] as unknown as Fragment[]) : [],
602
+ { recordMessage: true },
467
603
  );
604
+ if (result?.messageId) recordThinkingIfLinked(result.messageId);
468
605
  }
469
606
  return;
470
607
  }
@@ -478,14 +615,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
478
615
  log?.info?.(
479
616
  `[${account.accountId}] openclaw-clawchat mid-stream media emitted as separate message (count=${mediaFragments.length})`,
480
617
  );
481
- await sendOpenclawClawlingText({
482
- client,
483
- account,
484
- to: target,
485
- text: "",
486
- mediaFragments,
487
- log,
488
- });
618
+ await sendStatic("", mediaFragments, [], { recordMessage: true });
489
619
  }
490
620
  }
491
621
  } else {
@@ -493,7 +623,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
493
623
  const richFragments =
494
624
  richFragment && account.richInteractions ? ([richFragment] as unknown as Fragment[]) : [];
495
625
  if (text.trim() || mediaFragments.length > 0 || richFragments.length > 0) {
496
- await sendStatic(text, mediaFragments, richFragments);
626
+ await sendStatic(text, mediaFragments, richFragments, { recordMessage: true });
497
627
  }
498
628
  }
499
629
  },
@@ -563,6 +693,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
563
693
  dispatcher: base.dispatcher,
564
694
  replyOptions: {
565
695
  ...base.replyOptions,
696
+ sourceReplyDeliveryMode: "automatic",
697
+ ...(streamingEnabled ? { disableBlockStreaming: false } : {}),
566
698
  ...streamingHooks,
567
699
  },
568
700
  markDispatchIdle: base.markDispatchIdle,