@clawling/clawchat-plugin-openclaw 2026.5.12-32 → 2026.5.12-39

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.5.12-32",
3
+ "version": "2026.5.12-39",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -13,6 +13,7 @@ This skill guides agent behavior for ClawChat-aware tasks. Use the registered Cl
13
13
 
14
14
  - Use registered ClawChat plugin tools for account/profile, friends, users, moments, comments, reactions, avatar, media, and read-only conversation lookup.
15
15
  - If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin.
16
+ - Use the `/clawchat-output` slash command when the user asks to change how much ClawChat runtime output is shown in the current conversation.
16
17
 
17
18
  ## OpenClaw CLI
18
19
 
@@ -30,6 +31,18 @@ Use `update --force` only when local ClawChat plugin or skill files look corrupt
30
31
 
31
32
  If `channels add` reports `Unknown channel: clawchat-plugin-openclaw`, use the runtime slash command `/clawchat-activate CODE` after the operator ensures the plugin is loaded.
32
33
 
34
+ ## Output Visibility
35
+
36
+ When the user asks to change ClawChat output verbosity, use the runtime slash command for the current conversation. Treat natural-language wording as aliases for the three supported modes:
37
+
38
+ | User wording | Command |
39
+ | --- | --- |
40
+ | quiet mode, silent mode, minimal output, final-only output, `minimal` | `/clawchat-output minimal` |
41
+ | conversation mode, normal mode, regular mode, default output, `normal` | `/clawchat-output normal` |
42
+ | dev mode, developer mode, verbose mode, full output, `full` | `/clawchat-output full` |
43
+
44
+ Do not edit config files directly for this request. If the slash command returns an error, report that error instead of claiming the mode changed.
45
+
33
46
  ## Plugin Tool Routing
34
47
 
35
48
  Tool descriptions are authoritative. These routing hints resolve common ambiguity:
@@ -57,7 +70,6 @@ Tool descriptions are authoritative. These routing hints resolve common ambiguit
57
70
  | Reply to an existing comment | `clawchat_reply_moment_comment` with `replyToCommentId` |
58
71
  | Delete a comment/reply | `clawchat_delete_moment_comment` with exact `momentId` and `commentId` |
59
72
  | Nickname or bio update | `clawchat_update_account_profile` |
60
- | Standalone shareable media URL | `clawchat_upload_media_file` |
61
73
 
62
74
  ## Profile And Identity Sync
63
75
 
package/src/api-client.ts CHANGED
@@ -69,7 +69,7 @@ export interface OpenclawClawlingApiClient {
69
69
  uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
70
70
  /**
71
71
  * Exchange an invite code for an agent token.
72
- * Request body shape: `{ code, platform, type }`.
72
+ * Request body shape: `{ code, platform, type, user_id? }`.
73
73
  */
74
74
  agentsConnect(params: {
75
75
  /** The invite code entered by the operator. */
@@ -78,6 +78,8 @@ export interface OpenclawClawlingApiClient {
78
78
  platform: string;
79
79
  /** Agent type tag (e.g. "bot"). */
80
80
  type: string;
81
+ /** Existing configured ClawChat user id, when re-activating an account. */
82
+ user_id?: string;
81
83
  }): Promise<AgentConnectResult>;
82
84
  /**
83
85
  * Upload an avatar image via `POST /v1/files/upload-url`. The resulting
@@ -423,7 +425,7 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
423
425
  },
424
426
  );
425
427
  },
426
- async agentsConnect({ code: inviteCode, platform, type }): Promise<AgentConnectResult> {
428
+ async agentsConnect({ code: inviteCode, platform, type, user_id: userId }): Promise<AgentConnectResult> {
427
429
  if (!inviteCode?.trim()) {
428
430
  throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
429
431
  }
@@ -433,14 +435,18 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
433
435
  if (!type?.trim()) {
434
436
  throw new ClawlingApiError("validation", "agentsConnect: type is required");
435
437
  }
438
+ const body: Record<string, string> = {
439
+ code: inviteCode.trim(),
440
+ platform: platform.trim(),
441
+ type: type.trim(),
442
+ };
443
+ if (userId?.trim()) {
444
+ body.user_id = userId.trim();
445
+ }
436
446
  return await call<AgentConnectResult>("POST", "/v1/agents/connect", {
437
447
  // `X-Device-Id` is added globally via `authHeaders` on every request.
438
448
  headers: { "content-type": "application/json" },
439
- body: JSON.stringify({
440
- code: inviteCode.trim(),
441
- platform: platform.trim(),
442
- type: type.trim(),
443
- }),
449
+ body: JSON.stringify(body),
444
450
  });
445
451
  },
446
452
  async uploadMedia(params): Promise<UploadResult> {
package/src/client.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { createHash } from "node:crypto";
2
+ import os from "node:os";
1
3
  import type { Envelope, Transport } from "./protocol-types.ts";
2
4
  import { createClawChatClient, type ClawlingChatClient } from "./ws-client.ts";
3
- import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
5
+ import { CHANNEL_ID, type ResolvedOpenclawClawlingAccount } from "./config.ts";
4
6
 
5
7
  export type { ChatType } from "./protocol-types.ts";
6
8
 
@@ -15,6 +17,12 @@ export interface CreateClientOverrides {
15
17
  };
16
18
  }
17
19
 
20
+ export function resolveOpenclawClawlingDeviceId(account: ResolvedOpenclawClawlingAccount): string {
21
+ const material = [CHANNEL_ID, account.accountId, account.userId, os.hostname()].join("\0");
22
+ const digest = createHash("sha256").update(material).digest("hex").slice(0, 24);
23
+ return `${CHANNEL_ID}-${digest}`;
24
+ }
25
+
18
26
  export function createOpenclawClawlingClient(
19
27
  account: ResolvedOpenclawClawlingAccount,
20
28
  overrides: CreateClientOverrides = {},
@@ -22,7 +30,7 @@ export function createOpenclawClawlingClient(
22
30
  const client = createClawChatClient({
23
31
  url: account.websocketUrl,
24
32
  token: account.token,
25
- deviceId: account.userId,
33
+ deviceId: resolveOpenclawClawlingDeviceId(account),
26
34
  ...(overrides.transport ? { transport: overrides.transport } : {}),
27
35
  reconnect: {
28
36
  enabled: true,
package/src/commands.ts CHANGED
@@ -78,7 +78,7 @@ function persistOutputVisibility(
78
78
  function formatOutputVisibilityResult(outputVisibility: OutputVisibility): string {
79
79
  const detailLevel: Record<OutputVisibility, string> = {
80
80
  minimal: "final only",
81
- normal: "final plus block media",
81
+ normal: "final plus block output",
82
82
  full: "final plus buffered reasoning, tool/progress, and block output",
83
83
  };
84
84
  return [
@@ -202,10 +202,12 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
202
202
  runtime.log("Verifying invite code …");
203
203
  let result;
204
204
  try {
205
+ const existingUserId = account.userId.trim();
205
206
  result = await apiClient.agentsConnect({
206
207
  code: inviteCode,
207
208
  platform: AGENTS_CONNECT_PLATFORM,
208
209
  type: AGENTS_CONNECT_TYPE,
210
+ ...(existingUserId ? { user_id: existingUserId } : {}),
209
211
  });
210
212
  } catch (err) {
211
213
  if (err instanceof ClawlingApiError) {
@@ -69,6 +69,22 @@ type ClawChatReplyOptions = TypedReplyDispatcherResult["replyOptions"] &
69
69
  {
70
70
  sourceReplyDeliveryMode: "automatic";
71
71
  disableBlockStreaming: boolean;
72
+ suppressDefaultToolProgressMessages: boolean;
73
+ allowProgressCallbacksWhenSourceDeliverySuppressed?: boolean;
74
+ onReasoningStream?: (payload: ReplyPayload) => void | Promise<void>;
75
+ onToolStart?: (payload: {
76
+ name?: string;
77
+ phase?: string;
78
+ args?: Record<string, unknown>;
79
+ detailMode?: "explain" | "raw";
80
+ }) => void | Promise<void>;
81
+ onToolResult?: (payload: ReplyPayload) => void | Promise<void>;
82
+ onItemEvent?: (payload: Record<string, unknown>) => void | Promise<void>;
83
+ onPlanUpdate?: (payload: Record<string, unknown>) => void | Promise<void>;
84
+ onCommandOutput?: (payload: Record<string, unknown>) => void | Promise<void>;
85
+ onPatchSummary?: (payload: Record<string, unknown>) => void | Promise<void>;
86
+ onCompactionStart?: () => void | Promise<void>;
87
+ onCompactionEnd?: () => void | Promise<void>;
72
88
  };
73
89
 
74
90
  type RichAction = {
@@ -188,12 +204,171 @@ function resolvePayloadText(payload: ReplyPayload): string {
188
204
  return renderMessagePresentationFallbackText({ presentation, text: payload.text ?? null });
189
205
  }
190
206
 
207
+ const FULL_OUTPUT_SUMMARY_MAX = 600;
208
+
209
+ function truncateSummary(text: string): string {
210
+ const compact = text.replace(/\s+/g, " ").trim();
211
+ if (compact.length <= FULL_OUTPUT_SUMMARY_MAX) return compact;
212
+ return `${compact.slice(0, FULL_OUTPUT_SUMMARY_MAX - 1).trimEnd()}…`;
213
+ }
214
+
215
+ function summarizeValue(value: unknown): string {
216
+ if (value == null) return "";
217
+ if (typeof value === "string") {
218
+ const trimmed = value.trim();
219
+ if (!trimmed) return "";
220
+ try {
221
+ const parsed = JSON.parse(trimmed) as unknown;
222
+ if (parsed && typeof parsed === "object") return summarizeValue(parsed);
223
+ } catch {
224
+ // Plain text is already the best summary.
225
+ }
226
+ return truncateSummary(trimmed);
227
+ }
228
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
229
+ if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
230
+ if (typeof value === "object") {
231
+ const keys = Object.keys(value as Record<string, unknown>).slice(0, 6);
232
+ return keys.length ? `object with ${keys.join(", ")}` : "object";
233
+ }
234
+ return truncateSummary(String(value));
235
+ }
236
+
237
+ function readStringField(payload: Record<string, unknown>, field: string): string {
238
+ const value = payload[field];
239
+ return typeof value === "string" ? value.trim() : "";
240
+ }
241
+
242
+ function readPayloadCommand(payload: { args?: unknown }): string {
243
+ const args = payload.args;
244
+ if (!args || typeof args !== "object" || Array.isArray(args)) return "";
245
+ const command = (args as Record<string, unknown>).command;
246
+ return typeof command === "string" ? command.trim() : "";
247
+ }
248
+
249
+ function normalizeCommandLabel(value: string): string {
250
+ const trimmed = value.trim();
251
+ return trimmed.replace(/^command\s+/i, "").trim() || trimmed;
252
+ }
253
+
254
+ function isTerminalCommandOutput(payload: Record<string, unknown>): boolean {
255
+ const phase = readStringField(payload, "phase").toLowerCase();
256
+ const status = readStringField(payload, "status").toLowerCase();
257
+ return (
258
+ phase === "end" ||
259
+ phase === "error" ||
260
+ typeof payload.exitCode === "number" ||
261
+ status === "completed" ||
262
+ status === "ok" ||
263
+ status === "success" ||
264
+ status === "failed" ||
265
+ status === "error"
266
+ );
267
+ }
268
+
269
+ function isToolProgressItem(payload: Record<string, unknown>): boolean {
270
+ const kind = readStringField(payload, "kind").toLowerCase();
271
+ const title = readStringField(payload, "title").toLowerCase();
272
+ const name = readStringField(payload, "name").toLowerCase();
273
+ const progressText = readStringField(payload, "progressText").toLowerCase();
274
+ return (
275
+ kind === "tool" ||
276
+ kind === "command" ||
277
+ title.startsWith("exec ") ||
278
+ title.startsWith("command ") ||
279
+ name.startsWith("exec ") ||
280
+ name.startsWith("command ") ||
281
+ progressText.startsWith("exec ") ||
282
+ progressText.startsWith("command ")
283
+ );
284
+ }
285
+
286
+ function isDefaultToolResultText(text: string): boolean {
287
+ return /^[🛠🔧]/u.test(text.trim());
288
+ }
289
+
290
+ function summarizeProgressPayload(payload: Record<string, unknown>): string {
291
+ return (
292
+ readStringField(payload, "progressText") ||
293
+ readStringField(payload, "summary") ||
294
+ readStringField(payload, "message") ||
295
+ readStringField(payload, "title") ||
296
+ readStringField(payload, "name") ||
297
+ readStringField(payload, "status") ||
298
+ summarizeValue(payload)
299
+ );
300
+ }
301
+
302
+ function formatToolStartSummary(payload: {
303
+ name?: string;
304
+ phase?: string;
305
+ args?: unknown;
306
+ }): string {
307
+ const name = payload.name?.trim() || "tool";
308
+ const phase = payload.phase?.trim();
309
+ if (phase && phase !== "start") return "";
310
+ const command = readPayloadCommand(payload);
311
+ if ((name === "exec" || name === "command") && !command) return "";
312
+ return `[tool] ${[name, command].filter(Boolean).join(" ")} started`;
313
+ }
314
+
315
+ function formatCommandOutputSummary(payload: Record<string, unknown>): string {
316
+ if (!isTerminalCommandOutput(payload)) return "";
317
+ const name = normalizeCommandLabel(
318
+ readStringField(payload, "title") || readStringField(payload, "name") || "command",
319
+ );
320
+ const status = readStringField(payload, "status").toLowerCase();
321
+ const visibleStatus =
322
+ status && status !== "ok" && status !== "completed" && status !== "success" ? status : "";
323
+ const exitCode = typeof payload.exitCode === "number" ? ` exit ${payload.exitCode}` : "";
324
+ const output = summarizeValue(payload.output);
325
+ const prefix = `[command] ${[name, visibleStatus].filter(Boolean).join(" ")}${exitCode}`;
326
+ return truncateSummary(output ? `${prefix}: ${output}` : prefix);
327
+ }
328
+
329
+ function formatPatchSummary(payload: Record<string, unknown>): string {
330
+ const summary = readStringField(payload, "summary");
331
+ if (summary) return `[patch] ${truncateSummary(summary)}`;
332
+ const parts: string[] = [];
333
+ for (const key of ["added", "modified", "deleted"]) {
334
+ const value = payload[key];
335
+ if (Array.isArray(value) && value.length > 0) parts.push(`${key}: ${value.map(String).join(", ")}`);
336
+ else if (typeof value === "number" && value > 0) parts.push(`${key}: ${value}`);
337
+ }
338
+ return `[patch] ${truncateSummary(parts.join("; ") || "updated")}`;
339
+ }
340
+
341
+ function formatItemEventSummary(payload: Record<string, unknown>): string {
342
+ const kind = readStringField(payload, "kind") || "progress";
343
+ const title = readStringField(payload, "title") || readStringField(payload, "name");
344
+ const status = readStringField(payload, "status");
345
+ const phase = readStringField(payload, "phase");
346
+ const summary = summarizeProgressPayload(payload);
347
+ const label = [title, status || phase].filter(Boolean).join(" ");
348
+ return `[${kind}] ${truncateSummary(label || summary || "activity")}`;
349
+ }
350
+
351
+ function formatPlanSummary(payload: Record<string, unknown>): string {
352
+ const title = readStringField(payload, "title") || "plan";
353
+ const explanation = readStringField(payload, "explanation");
354
+ const steps = Array.isArray(payload.steps) ? payload.steps.map(String).filter(Boolean) : [];
355
+ return `[plan] ${truncateSummary([title, explanation, ...steps].filter(Boolean).join(": "))}`;
356
+ }
357
+
358
+ function formatApprovalSummary(payload: Record<string, unknown>): string {
359
+ const title = readStringField(payload, "title") || readStringField(payload, "kind") || "approval";
360
+ const status = readStringField(payload, "status") || readStringField(payload, "phase");
361
+ const message = readStringField(payload, "message") || readStringField(payload, "reason");
362
+ return `[approval] ${truncateSummary([title, status, message].filter(Boolean).join(" "))}`;
363
+ }
364
+
191
365
  /**
192
366
  * Reply dispatcher for clawchat-plugin-openclaw.
193
367
  *
194
368
  * ClawChat emits only materialized `message.send` / `message.reply` frames for
195
- * the final reply. Non-final OpenClaw deliveries are ignored or buffered
196
- * according to outputVisibility and are never sent as separate ClawChat frames.
369
+ * complete OpenClaw output units. `disableBlockStreaming` prevents token/block
370
+ * streaming; full visibility still forwards complete tool/progress/output units
371
+ * as separate ClawChat messages.
197
372
  */
198
373
  export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOptions): {
199
374
  dispatcher: TypedReplyDispatcherResult["dispatcher"];
@@ -213,6 +388,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
213
388
  } = options;
214
389
  const isGroupTarget = target.chatType === "group";
215
390
  const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
391
+ const splitFullOutput = outputVisibility === "full";
392
+ const splitNormalBlockOutput = outputVisibility === "normal";
216
393
  const ownerDirectTarget = () => {
217
394
  const ownerUserId = account.ownerUserId?.trim();
218
395
  return ownerUserId ? { chatId: ownerUserId, chatType: "direct" as const } : null;
@@ -245,10 +422,13 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
245
422
 
246
423
  let reasoningText = "";
247
424
  let bufferedOutputText = "";
425
+ const bufferedOutputLineSet = new Set<string>();
426
+ const emittedFullSegmentSet = new Set<string>();
248
427
  const bufferedOutputUrls: string[] = [];
249
428
  let runDone = false;
250
429
  let typingActive = false;
251
430
  let terminalReplySuppressed = false;
431
+ let finalDeliverySeen = false;
252
432
 
253
433
  const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
254
434
  const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
@@ -331,13 +511,13 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
331
511
  recordOutbound("thinking", messageId, thinkingText);
332
512
  reasoningText = "";
333
513
  };
334
- const resetBufferedOutput = () => {
335
- bufferedOutputText = "";
336
- bufferedOutputUrls.length = 0;
337
- };
338
514
  const appendBufferedText = (value: string) => {
339
515
  const trimmed = value.trim();
340
516
  if (!trimmed) return;
517
+ if (!trimmed.includes("\n")) {
518
+ if (bufferedOutputLineSet.has(trimmed)) return;
519
+ bufferedOutputLineSet.add(trimmed);
520
+ }
341
521
  bufferedOutputText = bufferedOutputText ? `${bufferedOutputText}\n${trimmed}` : trimmed;
342
522
  };
343
523
  const appendBufferedUrls = (urls: string[]) => {
@@ -345,12 +525,17 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
345
525
  if (url && !bufferedOutputUrls.includes(url)) bufferedOutputUrls.push(url);
346
526
  }
347
527
  };
528
+ const fullSegmentKey = (text: string, urls: string[]): string =>
529
+ JSON.stringify({
530
+ text: text.replace(/\s+/g, " ").trim(),
531
+ urls: urls.filter(Boolean),
532
+ });
348
533
  const mergeFinalText = (text: string): string => {
349
- if (outputVisibility !== "full") return text;
534
+ if (outputVisibility === "minimal" || outputVisibility === "full") return text;
350
535
  return [bufferedOutputText.trim(), text.trim()].filter(Boolean).join("\n");
351
536
  };
352
537
  const mergeFinalUrls = (urls: string[]): string[] => {
353
- if (outputVisibility === "minimal") return urls;
538
+ if (outputVisibility === "minimal" || outputVisibility === "full") return urls;
354
539
  const merged = bufferedOutputUrls.slice();
355
540
  for (const url of urls) {
356
541
  if (url && !merged.includes(url)) merged.push(url);
@@ -485,15 +670,42 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
485
670
  return result;
486
671
  };
487
672
 
673
+ const emitFullSegment = async (text: string, urls: string[] = []): Promise<void> => {
674
+ if (outputVisibility !== "full" && !splitNormalBlockOutput) {
675
+ appendBufferedText(text);
676
+ appendBufferedUrls(urls);
677
+ return;
678
+ }
679
+ if (!splitFullOutput && !splitNormalBlockOutput) {
680
+ appendBufferedText(text);
681
+ appendBufferedUrls(urls);
682
+ return;
683
+ }
684
+ const trimmed = text.trim();
685
+ if (!trimmed && urls.length === 0) return;
686
+ const segmentKey = fullSegmentKey(trimmed, urls);
687
+ if (emittedFullSegmentSet.has(segmentKey)) return;
688
+ emittedFullSegmentSet.add(segmentKey);
689
+ const mediaFragments = await uploadMediaUrls(urls);
690
+ await sendStatic(trimmed, mediaFragments, [], { recordMessage: true });
691
+ };
692
+
693
+ const emitFullRuntimeText = async (
694
+ label: string,
695
+ text: string,
696
+ urls: string[] = [],
697
+ ): Promise<void> => {
698
+ const summary = summarizeValue(text);
699
+ if (!summary && urls.length === 0) return;
700
+ await emitFullSegment(summary ? `[${label}] ${summary}` : "", urls);
701
+ };
702
+
488
703
  // ----- Dispatcher -------------------------------------------------------
489
704
 
490
705
  const base = runtime.channel.reply.createReplyDispatcherWithTyping({
491
706
  humanDelay,
492
707
  onReplyStart: async () => {
493
708
  emitTyping(true);
494
- reasoningText = "";
495
- resetBufferedOutput();
496
- runDone = false;
497
709
  },
498
710
  deliver: async (payload: ReplyPayload, info?: { kind: "tool" | "block" | "final" }) => {
499
711
  if (consumeTerminalSend(info?.kind ?? "unknown")) return;
@@ -505,6 +717,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
505
717
  );
506
718
 
507
719
  if (isGroupTarget && richFragment) {
720
+ if (info?.kind === "final") finalDeliverySeen = true;
508
721
  if (info?.kind !== "final") return;
509
722
  await sendOwnerAttention(
510
723
  resolvePayloadText(payload),
@@ -514,29 +727,33 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
514
727
  }
515
728
 
516
729
  if (isGroupTarget && info?.kind === "final" && looksLikeApprovalFallbackText(text)) {
730
+ finalDeliverySeen = true;
517
731
  await sendOwnerAttention(text);
518
732
  return;
519
733
  }
520
734
 
521
735
  if (payload.isReasoning) {
522
- if (isGroupTarget || outputVisibility !== "full") return;
523
- appendBufferedText(text);
736
+ if (outputVisibility !== "full") return;
737
+ await emitFullSegment(text, urls);
524
738
  const trimmed = text.trim();
525
739
  if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
526
740
  return;
527
741
  }
528
742
 
529
743
  if (info?.kind === "tool") {
530
- if (!isGroupTarget && outputVisibility === "full") {
531
- appendBufferedText(text);
532
- appendBufferedUrls(urls);
744
+ if (isDefaultToolResultText(text)) return;
745
+ if (outputVisibility === "full") {
746
+ await emitFullRuntimeText("tool result", text, urls);
533
747
  }
534
748
  return;
535
749
  }
536
750
 
537
751
  if (info?.kind === "block") {
538
- if (!isGroupTarget && outputVisibility === "normal") appendBufferedUrls(urls);
539
- if (!isGroupTarget && outputVisibility === "full") {
752
+ if (outputVisibility === "full") {
753
+ await emitFullSegment(text, urls);
754
+ } else if (splitNormalBlockOutput) {
755
+ await emitFullSegment(text, urls);
756
+ } else if (outputVisibility === "minimal" || outputVisibility === "normal") {
540
757
  appendBufferedText(text);
541
758
  appendBufferedUrls(urls);
542
759
  }
@@ -544,6 +761,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
544
761
  }
545
762
 
546
763
  if (info?.kind === "final") {
764
+ finalDeliverySeen = true;
547
765
  if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
548
766
  log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
549
767
  openclawLlmContextDebug.writeSnapshot({
@@ -567,6 +785,14 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
567
785
  }
568
786
  const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
569
787
  const finalUrls = mergeFinalUrls(urls);
788
+ if (
789
+ isClawChatNoopResponseText(finalText) &&
790
+ !richFragment &&
791
+ finalUrls.length === 0
792
+ ) {
793
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
794
+ return;
795
+ }
570
796
  const mediaFragments = await uploadMediaUrls(finalUrls);
571
797
  const result = await sendStatic(
572
798
  finalText,
@@ -585,17 +811,19 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
585
811
  log?.error?.(
586
812
  `[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`,
587
813
  );
588
- if (isGroupTarget) {
589
- log?.error?.(
590
- `[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`,
591
- );
592
- return;
593
- }
814
+ if (outputVisibility === "full") void emitFullRuntimeText("error", errorText);
594
815
  },
595
816
  onIdle: async () => {
596
817
  emitTyping(false);
597
818
  if (runDone) return;
598
819
  runDone = true;
820
+ if (finalDeliverySeen) return;
821
+ const fallbackText = bufferedOutputText.trim();
822
+ const fallbackUrls = bufferedOutputUrls.slice();
823
+ if (!fallbackText && fallbackUrls.length === 0) return;
824
+ const mediaFragments = await uploadMediaUrls(fallbackUrls);
825
+ const result = await sendStatic(fallbackText, mediaFragments, [], { recordMessage: true });
826
+ if (result?.messageId) recordThinkingIfLinked(result.messageId);
599
827
  },
600
828
  onCleanup: () => {
601
829
  emitTyping(false);
@@ -607,7 +835,69 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
607
835
  replyOptions: {
608
836
  ...base.replyOptions,
609
837
  sourceReplyDeliveryMode: "automatic",
610
- disableBlockStreaming: outputVisibility !== "full",
838
+ disableBlockStreaming: !splitNormalBlockOutput,
839
+ suppressDefaultToolProgressMessages: true,
840
+ allowProgressCallbacksWhenSourceDeliverySuppressed: splitFullOutput ? true : undefined,
841
+ onReasoningStream: splitFullOutput
842
+ ? async (payload: ReplyPayload) => {
843
+ if (consumeTerminalSend("reasoning")) return;
844
+ const text = resolvePayloadText(payload);
845
+ await emitFullRuntimeText("reasoning", text, resolveOutboundMediaUrls(payload).filter(Boolean));
846
+ const trimmed = text.trim();
847
+ if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
848
+ }
849
+ : undefined,
850
+ onToolStart: splitFullOutput
851
+ ? async (payload) => {
852
+ if (consumeTerminalSend("tool-start")) return;
853
+ await emitFullSegment(formatToolStartSummary(payload));
854
+ }
855
+ : undefined,
856
+ onToolResult: splitFullOutput
857
+ ? async (payload: ReplyPayload) => {
858
+ if (consumeTerminalSend("tool-result")) return;
859
+ const text = resolvePayloadText(payload);
860
+ if (isDefaultToolResultText(text)) return;
861
+ await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
862
+ }
863
+ : undefined,
864
+ onItemEvent: splitFullOutput
865
+ ? async (payload: Record<string, unknown>) => {
866
+ if (consumeTerminalSend("item-event")) return;
867
+ if (isToolProgressItem(payload)) return;
868
+ await emitFullRuntimeText("progress", summarizeProgressPayload(payload));
869
+ }
870
+ : undefined,
871
+ onPlanUpdate: splitFullOutput
872
+ ? async (payload: Record<string, unknown>) => {
873
+ if (consumeTerminalSend("plan-update")) return;
874
+ await emitFullRuntimeText("plan", summarizeProgressPayload(payload));
875
+ }
876
+ : undefined,
877
+ onCommandOutput: splitFullOutput
878
+ ? async (payload: Record<string, unknown>) => {
879
+ if (consumeTerminalSend("command-output")) return;
880
+ await emitFullSegment(formatCommandOutputSummary(payload));
881
+ }
882
+ : undefined,
883
+ onPatchSummary: splitFullOutput
884
+ ? async (payload: Record<string, unknown>) => {
885
+ if (consumeTerminalSend("patch-summary")) return;
886
+ await emitFullSegment(formatPatchSummary(payload));
887
+ }
888
+ : undefined,
889
+ onCompactionStart: splitFullOutput
890
+ ? async () => {
891
+ if (consumeTerminalSend("compaction-start")) return;
892
+ await emitFullSegment("[compaction] started");
893
+ }
894
+ : undefined,
895
+ onCompactionEnd: splitFullOutput
896
+ ? async () => {
897
+ if (consumeTerminalSend("compaction-end")) return;
898
+ await emitFullSegment("[compaction] finished");
899
+ }
900
+ : undefined,
611
901
  },
612
902
  markDispatchIdle: base.markDispatchIdle,
613
903
  };