@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13

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 (85) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +121 -19
  3. package/dist/index.js +10 -19
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +78 -10
  6. package/dist/src/api-types.test-d.js +10 -0
  7. package/dist/src/channel.js +25 -156
  8. package/dist/src/channel.setup.js +120 -0
  9. package/dist/src/client.js +37 -41
  10. package/dist/src/config.js +75 -17
  11. package/dist/src/inbound.js +79 -61
  12. package/dist/src/login.runtime.js +84 -19
  13. package/dist/src/media-runtime.js +8 -8
  14. package/dist/src/message-mapper.js +1 -1
  15. package/dist/src/mock-transport.js +31 -0
  16. package/dist/src/outbound.js +410 -26
  17. package/dist/src/protocol-types.js +63 -0
  18. package/dist/src/protocol-types.typecheck.js +1 -0
  19. package/dist/src/protocol.js +2 -7
  20. package/dist/src/reply-dispatcher.js +157 -54
  21. package/dist/src/runtime.js +795 -119
  22. package/dist/src/storage.js +689 -0
  23. package/dist/src/tools-schema.js +98 -16
  24. package/dist/src/tools.js +422 -135
  25. package/dist/src/ws-alignment.js +178 -0
  26. package/dist/src/ws-client.js +588 -0
  27. package/dist/src/ws-log.js +19 -0
  28. package/index.ts +10 -22
  29. package/openclaw.plugin.json +37 -2
  30. package/package.json +17 -4
  31. package/setup-entry.ts +4 -0
  32. package/skills/clawchat/SKILL.md +88 -0
  33. package/src/api-client.test.ts +274 -14
  34. package/src/api-client.ts +138 -23
  35. package/src/api-types.test-d.ts +12 -0
  36. package/src/api-types.ts +90 -4
  37. package/src/buffered-stream.test.ts +14 -12
  38. package/src/buffered-stream.ts +1 -1
  39. package/src/channel.outbound.test.ts +269 -60
  40. package/src/channel.setup.ts +146 -0
  41. package/src/channel.test.ts +130 -24
  42. package/src/channel.ts +30 -186
  43. package/src/client.test.ts +197 -11
  44. package/src/client.ts +50 -57
  45. package/src/config.test.ts +108 -6
  46. package/src/config.ts +95 -24
  47. package/src/inbound.test.ts +288 -37
  48. package/src/inbound.ts +96 -84
  49. package/src/login.runtime.test.ts +347 -13
  50. package/src/login.runtime.ts +105 -23
  51. package/src/manifest.test.ts +146 -74
  52. package/src/media-runtime.test.ts +57 -2
  53. package/src/media-runtime.ts +26 -17
  54. package/src/message-mapper.test.ts +2 -2
  55. package/src/message-mapper.ts +2 -2
  56. package/src/mock-transport.test.ts +35 -0
  57. package/src/mock-transport.ts +38 -0
  58. package/src/outbound.test.ts +694 -73
  59. package/src/outbound.ts +484 -31
  60. package/src/plugin-entry.test.ts +1 -0
  61. package/src/protocol-types.test.ts +69 -0
  62. package/src/protocol-types.ts +296 -0
  63. package/src/protocol-types.typecheck.ts +89 -0
  64. package/src/protocol.test.ts +1 -6
  65. package/src/protocol.ts +2 -7
  66. package/src/reply-dispatcher.test.ts +819 -119
  67. package/src/reply-dispatcher.ts +202 -60
  68. package/src/runtime.test.ts +2120 -41
  69. package/src/runtime.ts +935 -142
  70. package/src/scripts.test.ts +85 -0
  71. package/src/storage.test.ts +793 -0
  72. package/src/storage.ts +1095 -0
  73. package/src/streaming.test.ts +9 -8
  74. package/src/streaming.ts +1 -1
  75. package/src/tools-schema.ts +148 -20
  76. package/src/tools.test.ts +377 -50
  77. package/src/tools.ts +574 -154
  78. package/src/ws-alignment.test.ts +103 -0
  79. package/src/ws-alignment.ts +275 -0
  80. package/src/ws-client.test.ts +1218 -0
  81. package/src/ws-client.ts +662 -0
  82. package/src/ws-log.test.ts +32 -0
  83. package/src/ws-log.ts +31 -0
  84. package/skills/clawchat-account-tools/SKILL.md +0 -26
  85. package/skills/clawchat-activate/SKILL.md +0 -47
@@ -1,11 +1,11 @@
1
1
  import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
2
+ import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
2
3
  import { createOpenclawClawlingApiClient } from "./api-client.js";
3
4
  import { openBufferedStreamingSession, mergeStreamingText, } from "./buffered-stream.js";
4
- import { emitFinalStreamReply } from "./client.js";
5
- import { textToFragments } from "./message-mapper.js";
6
5
  import { uploadOutboundMedia } from "./media-runtime.js";
7
- import { sendOpenclawClawlingText } from "./outbound.js";
6
+ import { sendOpenclawClawlingText, } from "./outbound.js";
8
7
  import { sendStreamingFailure } from "./streaming.js";
8
+ const CLIENT_SAFE_REPLY_FAILURE_TEXT = "OpenClaw could not complete this reply.";
9
9
  function normalizeReplyErrorText(error) {
10
10
  const raw = String(error);
11
11
  const retryWrapped = raw.match(/^Error: Retry failed for delivery [^:]+:\s*(.+)$/s);
@@ -117,7 +117,7 @@ function resolvePayloadText(payload) {
117
117
  * `message.reply` per deliver with text + media.
118
118
  */
119
119
  export function createOpenclawClawlingReplyDispatcher(options) {
120
- const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, inboundForFinalReply, log, } = options;
120
+ const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, inboundForFinalReply, store, log, } = options;
121
121
  const routing = { chatId: target.chatId, chatType: target.chatType };
122
122
  const humanDelay = runtime.channel.reply.resolveHumanDelayConfig(cfg, account.userId);
123
123
  const streamingEnabled = account.replyMode === "stream" && !replyCtx;
@@ -151,16 +151,87 @@ export function createOpenclawClawlingReplyDispatcher(options) {
151
151
  let streamingClosed = false;
152
152
  let runFailed = false;
153
153
  let runDone = false;
154
+ let streamClaimAttempted = false;
154
155
  // `streamCreatedEmitted` is the authoritative guard: once a `message.created`
155
156
  // has been emitted for this dispatcher instance, never emit another — even
156
157
  // if `onReplyStart` fires again or a pre-onReplyStart `onPartialReply`
157
158
  // raced the lazy open path.
158
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
+ };
159
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)}`;
160
231
  const openSessionIfNeeded = () => {
161
- if (!streamingEnabled || streamingSession || streamCreatedEmitted)
232
+ if (!streamingEnabled || streamingSession || streamCreatedEmitted || streamClaimAttempted)
162
233
  return;
163
- streamCreatedEmitted = true;
234
+ streamClaimAttempted = true;
164
235
  // Mint a fresh agent-side message_id at `message.created` time. All
165
236
  // subsequent `message.add` / `message.done` / `message.reply` frames for
166
237
  // this stream reuse it. Once the stream finalizes (done or reply), this
@@ -169,12 +240,20 @@ export function createOpenclawClawlingReplyDispatcher(options) {
169
240
  // `replyTo.msgId`; keeping the two distinct avoids the agent's reply
170
241
  // frames shadowing the user turn they answer.
171
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;
172
251
  streamingSession = openBufferedStreamingSession({
173
252
  client,
174
253
  routing,
175
254
  sender: {
176
255
  id: account.userId,
177
- type: target.chatType,
256
+ type: "direct",
178
257
  nick_name: account.userId,
179
258
  },
180
259
  messageId: streamingMessageId,
@@ -215,26 +294,49 @@ export function createOpenclawClawlingReplyDispatcher(options) {
215
294
  log?.info?.(`[${account.accountId}] openclaw-clawchat streaming closed msg=${streamingMessageId} reason=${reason ?? "done"}`);
216
295
  };
217
296
  // ----- Static send ------------------------------------------------------
218
- const sendStatic = async (text, mediaFragments = [], richFragments = []) => {
297
+ const sendStatic = async (text, mediaFragments = [], richFragments = [], options = {}) => {
219
298
  if (!text.trim() && mediaFragments.length === 0 && richFragments.length === 0)
220
- return;
299
+ return null;
221
300
  log?.info?.(`[${account.accountId}] openclaw-clawchat sending static text_len=${text.length} media=${mediaFragments.length} rich=${richFragments.length} to=${target.chatId}`);
222
- 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({
223
315
  client,
224
316
  account,
225
317
  to: target,
226
318
  text,
319
+ messageId,
227
320
  ...(replyCtx ? { replyCtx } : {}),
228
321
  ...(richFragments.length > 0 ? { richFragments } : {}),
229
322
  ...(mediaFragments.length > 0 ? { mediaFragments } : {}),
230
323
  log,
231
324
  });
232
325
  log?.info?.(`[${account.accountId}] openclaw-clawchat send complete to=${target.chatId}`);
326
+ return result;
327
+ };
328
+ const logDetachedFailure = (action, error) => {
329
+ log?.error?.(`[${account.accountId}] openclaw-clawchat ${action} failed: ${String(error)}`);
330
+ };
331
+ const settleDetached = (action, promise) => {
332
+ void promise.catch((error) => logDetachedFailure(action, error));
233
333
  };
234
334
  const emitFinalConsolidatedMessage = async () => {
235
335
  if (finalEmitted)
236
336
  return;
237
337
  finalEmitted = true;
338
+ if (!streamingMessageId)
339
+ return;
238
340
  const mergedMedia = await uploadMediaUrls(accumulatedMediaUrls.slice());
239
341
  const mergedText = streamText.trim();
240
342
  if (!mergedText && finalRichFragments.length === 0 && mergedMedia.length === 0) {
@@ -242,28 +344,29 @@ export function createOpenclawClawlingReplyDispatcher(options) {
242
344
  return;
243
345
  }
244
346
  log?.info?.(`[${account.accountId}] openclaw-clawchat emitting consolidated final (message.reply) msg=${streamingMessageId} text_len=${mergedText.length} media=${mergedMedia.length}`);
245
- const bodyFragments = [
246
- ...(mergedText ? textToFragments(mergedText) : []),
247
- ...finalRichFragments,
248
- // mediaFragments is the local wide shape; cast at SDK boundary as
249
- // we do in outbound.ts.
250
- ...mergedMedia,
251
- ];
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
+ };
252
354
  // Streaming message_id must match the created/add/done frames so the
253
- // backend can correlate the consolidated reply with the stream.
254
- 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,
255
362
  messageId: streamingMessageId,
256
- routing,
257
- replyTo: {
258
- msgId: inboundMessageId ?? streamingMessageId,
259
- previewId: inboundForFinalReply?.chatId ?? target.chatId,
260
- nickName: inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
261
- fragments: inboundForFinalReply?.bodyText
262
- ? [{ kind: "text", text: inboundForFinalReply.bodyText }]
263
- : [],
264
- },
265
- body: { fragments: bodyFragments },
363
+ replyCtx: finalReplyCtx,
364
+ ...(finalRichFragments.length > 0 ? { richFragments: finalRichFragments } : {}),
365
+ ...(mergedMedia.length > 0 ? { mediaFragments: mergedMedia } : {}),
366
+ log,
266
367
  });
368
+ updateOutbound("message.reply", streamingMessageId, mergedText, { target, replyCtx: replyCtx ?? null, mode: "stream-final" });
369
+ recordThinkingIfLinked(streamingMessageId);
267
370
  };
268
371
  const ingestFinalPayload = (payload, text, richFragment) => {
269
372
  if (richFragment && account.richInteractions) {
@@ -271,10 +374,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
271
374
  }
272
375
  if (text)
273
376
  streamText = mergeStreamingText(streamText, text);
274
- const urls = [
275
- ...(payload.mediaUrl ? [payload.mediaUrl] : []),
276
- ...(payload.mediaUrls ?? []),
277
- ].filter((u) => Boolean(u));
377
+ const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
278
378
  for (const url of urls) {
279
379
  if (!accumulatedMediaUrls.includes(url))
280
380
  accumulatedMediaUrls.push(url);
@@ -313,10 +413,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
313
413
  deliver: async (payload, info) => {
314
414
  const richFragment = buildRichInteractionFragment(payload);
315
415
  const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
316
- const urls = [
317
- ...(payload.mediaUrl ? [payload.mediaUrl] : []),
318
- ...(payload.mediaUrls ?? []),
319
- ].filter((u) => Boolean(u));
416
+ const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
320
417
  log?.info?.(`[${account.accountId}] openclaw-clawchat deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`);
321
418
  if (payload.isReasoning) {
322
419
  if (!account.forwardThinking)
@@ -326,7 +423,10 @@ export function createOpenclawClawlingReplyDispatcher(options) {
326
423
  await queueStreamSnapshot();
327
424
  }
328
425
  else {
329
- await sendStatic(text);
426
+ reasoningText = mergeStreamingText(reasoningText, text);
427
+ const result = await sendStatic(text, [], [], { recordMessage: true });
428
+ if (result?.messageId)
429
+ recordThinkingIfLinked(result.messageId);
330
430
  }
331
431
  return;
332
432
  }
@@ -336,9 +436,19 @@ export function createOpenclawClawlingReplyDispatcher(options) {
336
436
  ingestFinalPayload(payload, text, richFragment && account.richInteractions ? richFragment : null);
337
437
  // For streaming: consolidated final is emitted in onIdle after done.
338
438
  // For static: emit immediately.
339
- 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 {
340
448
  const mediaFragments = await uploadMediaUrls(urls);
341
- 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);
342
452
  }
343
453
  return;
344
454
  }
@@ -350,14 +460,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
350
460
  const mediaFragments = await uploadMediaUrls(urls);
351
461
  if (mediaFragments.length > 0) {
352
462
  log?.info?.(`[${account.accountId}] openclaw-clawchat mid-stream media emitted as separate message (count=${mediaFragments.length})`);
353
- await sendOpenclawClawlingText({
354
- client,
355
- account,
356
- to: target,
357
- text: "",
358
- mediaFragments,
359
- log,
360
- });
463
+ await sendStatic("", mediaFragments, [], { recordMessage: true });
361
464
  }
362
465
  }
363
466
  }
@@ -365,7 +468,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
365
468
  const mediaFragments = await uploadMediaUrls(urls);
366
469
  const richFragments = richFragment && account.richInteractions ? [richFragment] : [];
367
470
  if (text.trim() || mediaFragments.length > 0 || richFragments.length > 0) {
368
- await sendStatic(text, mediaFragments, richFragments);
471
+ await sendStatic(text, mediaFragments, richFragments, { recordMessage: true });
369
472
  }
370
473
  }
371
474
  },
@@ -373,12 +476,11 @@ export function createOpenclawClawlingReplyDispatcher(options) {
373
476
  const errorText = normalizeReplyErrorText(error);
374
477
  log?.error?.(`[${account.accountId}] openclaw-clawchat ${info.kind} reply failed: ${errorText}`);
375
478
  if (!streamingEnabled) {
376
- void sendStatic(errorText);
377
479
  return;
378
480
  }
379
481
  runFailed = true;
380
482
  if (streamingSession && !streamingClosed) {
381
- void closeStreamingSession("fail", errorText);
483
+ settleDetached("stream failure close", closeStreamingSession("fail", CLIENT_SAFE_REPLY_FAILURE_TEXT));
382
484
  return;
383
485
  }
384
486
  if (streamingClosed)
@@ -386,13 +488,13 @@ export function createOpenclawClawlingReplyDispatcher(options) {
386
488
  streamingClosed = true;
387
489
  if (!streamingMessageId)
388
490
  streamingMessageId = mintStreamingMessageId();
389
- void sendStreamingFailure({
491
+ settleDetached("stream failure send", sendStreamingFailure({
390
492
  client,
391
493
  routing,
392
494
  messageId: streamingMessageId,
393
495
  currentSequence: 0,
394
- reason: errorText,
395
- });
496
+ reason: CLIENT_SAFE_REPLY_FAILURE_TEXT,
497
+ }));
396
498
  },
397
499
  onIdle: async () => {
398
500
  if (runDone)
@@ -433,6 +535,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
433
535
  dispatcher: base.dispatcher,
434
536
  replyOptions: {
435
537
  ...base.replyOptions,
538
+ sourceReplyDeliveryMode: "automatic",
436
539
  ...streamingHooks,
437
540
  },
438
541
  markDispatchIdle: base.markDispatchIdle,