@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
@@ -2,10 +2,8 @@ import { interactiveReplyToPresentation, renderMessagePresentationFallbackText,
2
2
  import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
3
3
  import { createOpenclawClawlingApiClient } from "./api-client.js";
4
4
  import { openBufferedStreamingSession, mergeStreamingText, } from "./buffered-stream.js";
5
- import { emitFinalStreamReply } from "./client.js";
6
- import { textToFragments } from "./message-mapper.js";
7
5
  import { uploadOutboundMedia } from "./media-runtime.js";
8
- import { sendOpenclawClawlingText } from "./outbound.js";
6
+ import { sendOpenclawClawlingText, } from "./outbound.js";
9
7
  import { sendStreamingFailure } from "./streaming.js";
10
8
  const CLIENT_SAFE_REPLY_FAILURE_TEXT = "OpenClaw could not complete this reply.";
11
9
  function normalizeReplyErrorText(error) {
@@ -119,7 +117,7 @@ function resolvePayloadText(payload) {
119
117
  * `message.reply` per deliver with text + media.
120
118
  */
121
119
  export function createOpenclawClawlingReplyDispatcher(options) {
122
- const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, inboundForFinalReply, log, } = options;
120
+ const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, inboundForFinalReply, store, log, } = options;
123
121
  const routing = { chatId: target.chatId, chatType: target.chatType };
124
122
  const humanDelay = runtime.channel.reply.resolveHumanDelayConfig(cfg, account.userId);
125
123
  const streamingEnabled = account.replyMode === "stream" && !replyCtx;
@@ -153,16 +151,87 @@ export function createOpenclawClawlingReplyDispatcher(options) {
153
151
  let streamingClosed = false;
154
152
  let runFailed = false;
155
153
  let runDone = false;
154
+ let streamClaimAttempted = false;
156
155
  // `streamCreatedEmitted` is the authoritative guard: once a `message.created`
157
156
  // has been emitted for this dispatcher instance, never emit another — even
158
157
  // if `onReplyStart` fires again or a pre-onReplyStart `onPartialReply`
159
158
  // raced the lazy open path.
160
159
  let streamCreatedEmitted = false;
160
+ const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
161
+ const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
162
+ const claimOutbound = (eventType, messageId, text, raw) => {
163
+ if (!store || !messageId)
164
+ return null;
165
+ try {
166
+ return store.claimMessageOnce({
167
+ platform: "openclaw",
168
+ accountId: account.accountId,
169
+ kind: "message",
170
+ direction: "outbound",
171
+ eventType,
172
+ chatId: target.chatId,
173
+ messageId,
174
+ text,
175
+ raw,
176
+ });
177
+ }
178
+ catch {
179
+ log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound claim failed`);
180
+ return null;
181
+ }
182
+ };
183
+ const updateOutbound = (eventType, messageId, text, raw) => {
184
+ if (!store || !messageId)
185
+ return;
186
+ try {
187
+ store.updateMessageByIdentity({
188
+ accountId: account.accountId,
189
+ kind: "message",
190
+ direction: "outbound",
191
+ eventType,
192
+ chatId: target.chatId,
193
+ messageId,
194
+ text,
195
+ raw,
196
+ });
197
+ }
198
+ catch {
199
+ log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound update failed; continuing`);
200
+ }
201
+ };
202
+ const recordOutbound = (kind, messageId, text) => {
203
+ if (!store || !messageId)
204
+ return;
205
+ try {
206
+ store.insertMessage({
207
+ platform: "openclaw",
208
+ accountId: account.accountId,
209
+ kind,
210
+ direction: "outbound",
211
+ eventType: outboundEventType(),
212
+ chatId: target.chatId,
213
+ messageId,
214
+ text,
215
+ raw: outboundRaw(),
216
+ });
217
+ }
218
+ catch {
219
+ log?.error?.(`[${account.accountId}] openclaw-clawchat sqlite outbound insert failed; continuing`);
220
+ }
221
+ };
222
+ const recordThinkingIfLinked = (messageId) => {
223
+ const thinkingText = reasoningText.trim();
224
+ if (!thinkingText)
225
+ return;
226
+ recordOutbound("thinking", messageId, thinkingText);
227
+ reasoningText = "";
228
+ };
161
229
  const mintStreamingMessageId = () => `${account.userId}-stream-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
230
+ const mintStaticMessageId = () => `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
162
231
  const openSessionIfNeeded = () => {
163
- if (!streamingEnabled || streamingSession || streamCreatedEmitted)
232
+ if (!streamingEnabled || streamingSession || streamCreatedEmitted || streamClaimAttempted)
164
233
  return;
165
- streamCreatedEmitted = true;
234
+ streamClaimAttempted = true;
166
235
  // Mint a fresh agent-side message_id at `message.created` time. All
167
236
  // subsequent `message.add` / `message.done` / `message.reply` frames for
168
237
  // this stream reuse it. Once the stream finalizes (done or reply), this
@@ -171,6 +240,14 @@ export function createOpenclawClawlingReplyDispatcher(options) {
171
240
  // `replyTo.msgId`; keeping the two distinct avoids the agent's reply
172
241
  // frames shadowing the user turn they answer.
173
242
  streamingMessageId = mintStreamingMessageId();
243
+ const claimed = claimOutbound("message.created", streamingMessageId, "", { target, replyCtx: replyCtx ?? null, mode: "stream" });
244
+ if (claimed !== true) {
245
+ streamCreatedEmitted = false;
246
+ streamingMessageId = "";
247
+ log?.[claimed === false ? "info" : "error"]?.(`[${account.accountId}] openclaw-clawchat stream outbound skipped reason=${claimed === false ? "duplicate" : "claim_unavailable"}`);
248
+ return;
249
+ }
250
+ streamCreatedEmitted = true;
174
251
  streamingSession = openBufferedStreamingSession({
175
252
  client,
176
253
  routing,
@@ -217,21 +294,36 @@ export function createOpenclawClawlingReplyDispatcher(options) {
217
294
  log?.info?.(`[${account.accountId}] openclaw-clawchat streaming closed msg=${streamingMessageId} reason=${reason ?? "done"}`);
218
295
  };
219
296
  // ----- Static send ------------------------------------------------------
220
- const sendStatic = async (text, mediaFragments = [], richFragments = []) => {
297
+ const sendStatic = async (text, mediaFragments = [], richFragments = [], options = {}) => {
221
298
  if (!text.trim() && mediaFragments.length === 0 && richFragments.length === 0)
222
- return;
299
+ return null;
223
300
  log?.info?.(`[${account.accountId}] openclaw-clawchat sending static text_len=${text.length} media=${mediaFragments.length} rich=${richFragments.length} to=${target.chatId}`);
224
- await sendOpenclawClawlingText({
301
+ const messageId = mintStaticMessageId();
302
+ const raw = { target, replyCtx: replyCtx ?? null, mode: "static" };
303
+ const claimed = options.recordMessage
304
+ ? claimOutbound(outboundEventType(), messageId, text, raw)
305
+ : true;
306
+ if (claimed === false) {
307
+ log?.info?.(`[${account.accountId}] openclaw-clawchat outbound duplicate skipped msg=${messageId}`);
308
+ return null;
309
+ }
310
+ if (claimed === null) {
311
+ log?.error?.(`[${account.accountId}] openclaw-clawchat outbound skipped msg=${messageId} reason=claim_unavailable`);
312
+ return null;
313
+ }
314
+ const result = await sendOpenclawClawlingText({
225
315
  client,
226
316
  account,
227
317
  to: target,
228
318
  text,
319
+ messageId,
229
320
  ...(replyCtx ? { replyCtx } : {}),
230
321
  ...(richFragments.length > 0 ? { richFragments } : {}),
231
322
  ...(mediaFragments.length > 0 ? { mediaFragments } : {}),
232
323
  log,
233
324
  });
234
325
  log?.info?.(`[${account.accountId}] openclaw-clawchat send complete to=${target.chatId}`);
326
+ return result;
235
327
  };
236
328
  const logDetachedFailure = (action, error) => {
237
329
  log?.error?.(`[${account.accountId}] openclaw-clawchat ${action} failed: ${String(error)}`);
@@ -243,6 +335,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
243
335
  if (finalEmitted)
244
336
  return;
245
337
  finalEmitted = true;
338
+ if (!streamingMessageId)
339
+ return;
246
340
  const mergedMedia = await uploadMediaUrls(accumulatedMediaUrls.slice());
247
341
  const mergedText = streamText.trim();
248
342
  if (!mergedText && finalRichFragments.length === 0 && mergedMedia.length === 0) {
@@ -250,28 +344,29 @@ export function createOpenclawClawlingReplyDispatcher(options) {
250
344
  return;
251
345
  }
252
346
  log?.info?.(`[${account.accountId}] openclaw-clawchat emitting consolidated final (message.reply) msg=${streamingMessageId} text_len=${mergedText.length} media=${mergedMedia.length}`);
253
- const bodyFragments = [
254
- ...(mergedText ? textToFragments(mergedText) : []),
255
- ...finalRichFragments,
256
- // mediaFragments is the local wide shape; cast at SDK boundary as
257
- // we do in outbound.ts.
258
- ...mergedMedia,
259
- ];
347
+ const finalReplyCtx = {
348
+ replyToMessageId: inboundMessageId ?? streamingMessageId,
349
+ replyPreviewChatId: inboundForFinalReply?.chatId ?? target.chatId,
350
+ replyPreviewSenderId: inboundForFinalReply?.senderId ?? target.chatId,
351
+ replyPreviewNickName: inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
352
+ replyPreviewText: inboundForFinalReply?.bodyText ?? "",
353
+ };
260
354
  // Streaming message_id must match the created/add/done frames so the
261
- // backend can correlate the consolidated reply with the stream.
262
- emitFinalStreamReply(client, {
355
+ // backend can correlate the consolidated reply with the stream. Use the
356
+ // ackable send path so disconnects queue the final user-visible answer.
357
+ await sendOpenclawClawlingText({
358
+ client,
359
+ account,
360
+ to: target,
361
+ text: mergedText,
263
362
  messageId: streamingMessageId,
264
- routing,
265
- replyTo: {
266
- msgId: inboundMessageId ?? streamingMessageId,
267
- previewId: inboundForFinalReply?.senderId ?? target.chatId,
268
- nickName: inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
269
- fragments: inboundForFinalReply?.bodyText
270
- ? [{ kind: "text", text: inboundForFinalReply.bodyText }]
271
- : [],
272
- },
273
- body: { fragments: bodyFragments },
363
+ replyCtx: finalReplyCtx,
364
+ ...(finalRichFragments.length > 0 ? { richFragments: finalRichFragments } : {}),
365
+ ...(mergedMedia.length > 0 ? { mediaFragments: mergedMedia } : {}),
366
+ log,
274
367
  });
368
+ updateOutbound("message.reply", streamingMessageId, mergedText, { target, replyCtx: replyCtx ?? null, mode: "stream-final" });
369
+ recordThinkingIfLinked(streamingMessageId);
275
370
  };
276
371
  const ingestFinalPayload = (payload, text, richFragment) => {
277
372
  if (richFragment && account.richInteractions) {
@@ -328,7 +423,10 @@ export function createOpenclawClawlingReplyDispatcher(options) {
328
423
  await queueStreamSnapshot();
329
424
  }
330
425
  else {
331
- await sendStatic(text);
426
+ reasoningText = mergeStreamingText(reasoningText, text);
427
+ const result = await sendStatic(text, [], [], { recordMessage: true });
428
+ if (result?.messageId)
429
+ recordThinkingIfLinked(result.messageId);
332
430
  }
333
431
  return;
334
432
  }
@@ -338,9 +436,19 @@ export function createOpenclawClawlingReplyDispatcher(options) {
338
436
  ingestFinalPayload(payload, text, richFragment && account.richInteractions ? richFragment : null);
339
437
  // For streaming: consolidated final is emitted in onIdle after done.
340
438
  // For static: emit immediately.
341
- if (!streamingEnabled) {
439
+ if (streamingEnabled) {
440
+ if (text.trim()) {
441
+ await queueStreamSnapshot();
442
+ }
443
+ else if (richFragment || urls.length > 0) {
444
+ openSessionIfNeeded();
445
+ }
446
+ }
447
+ else {
342
448
  const mediaFragments = await uploadMediaUrls(urls);
343
- await sendStatic(text, mediaFragments, richFragment && account.richInteractions ? [richFragment] : []);
449
+ const result = await sendStatic(text, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
450
+ if (result?.messageId)
451
+ recordThinkingIfLinked(result.messageId);
344
452
  }
345
453
  return;
346
454
  }
@@ -352,14 +460,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
352
460
  const mediaFragments = await uploadMediaUrls(urls);
353
461
  if (mediaFragments.length > 0) {
354
462
  log?.info?.(`[${account.accountId}] openclaw-clawchat mid-stream media emitted as separate message (count=${mediaFragments.length})`);
355
- await sendOpenclawClawlingText({
356
- client,
357
- account,
358
- to: target,
359
- text: "",
360
- mediaFragments,
361
- log,
362
- });
463
+ await sendStatic("", mediaFragments, [], { recordMessage: true });
363
464
  }
364
465
  }
365
466
  }
@@ -367,7 +468,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
367
468
  const mediaFragments = await uploadMediaUrls(urls);
368
469
  const richFragments = richFragment && account.richInteractions ? [richFragment] : [];
369
470
  if (text.trim() || mediaFragments.length > 0 || richFragments.length > 0) {
370
- await sendStatic(text, mediaFragments, richFragments);
471
+ await sendStatic(text, mediaFragments, richFragments, { recordMessage: true });
371
472
  }
372
473
  }
373
474
  },
@@ -434,6 +535,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
434
535
  dispatcher: base.dispatcher,
435
536
  replyOptions: {
436
537
  ...base.replyOptions,
538
+ sourceReplyDeliveryMode: "automatic",
539
+ ...(streamingEnabled ? { disableBlockStreaming: false } : {}),
437
540
  ...streamingHooks,
438
541
  },
439
542
  markDispatchIdle: base.markDispatchIdle,