@clawling/clawchat-plugin-openclaw 2026.5.12-39 → 2026.5.13-dev.0
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/dist/src/protocol-types.js +2 -0
- package/dist/src/reply-dispatcher.js +19 -25
- package/dist/src/runtime.js +19 -1
- package/dist/src/ws-alignment.js +67 -0
- package/dist/src/ws-client.js +13 -1
- package/package.json +1 -1
- package/skills/clawchat/SKILL.md +0 -13
- package/src/protocol-types.ts +26 -0
- package/src/reply-dispatcher.ts +21 -26
- package/src/runtime.ts +21 -1
- package/src/ws-alignment.ts +97 -0
- package/src/ws-client.ts +11 -1
- package/dist/src/buffered-stream.js +0 -177
- package/dist/src/streaming.js +0 -65
|
@@ -13,6 +13,8 @@ export const EVENT = {
|
|
|
13
13
|
MESSAGE_FAILED: "message.failed",
|
|
14
14
|
TYPING_UPDATE: "typing.update",
|
|
15
15
|
CHAT_METADATA_INVALIDATED: "chat.metadata.invalidated",
|
|
16
|
+
NOTIFY_SIGNAL: "notify.signal",
|
|
17
|
+
REPLAY_DONE: "replay.done",
|
|
16
18
|
OFFLINE_BATCH: "offline.batch",
|
|
17
19
|
OFFLINE_ACK: "offline.ack",
|
|
18
20
|
OFFLINE_DONE: "offline.done",
|
|
@@ -259,8 +259,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
259
259
|
const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, log, } = options;
|
|
260
260
|
const isGroupTarget = target.chatType === "group";
|
|
261
261
|
const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
|
|
262
|
-
const splitFullOutput = outputVisibility === "full";
|
|
263
|
-
const splitNormalBlockOutput = outputVisibility === "normal";
|
|
262
|
+
const splitFullOutput = outputVisibility === "full" && !isGroupTarget;
|
|
264
263
|
const ownerDirectTarget = () => {
|
|
265
264
|
const ownerUserId = account.ownerUserId?.trim();
|
|
266
265
|
return ownerUserId ? { chatId: ownerUserId, chatType: "direct" } : null;
|
|
@@ -524,12 +523,12 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
524
523
|
return result;
|
|
525
524
|
};
|
|
526
525
|
const emitFullSegment = async (text, urls = []) => {
|
|
527
|
-
if (outputVisibility !== "full"
|
|
526
|
+
if (outputVisibility !== "full") {
|
|
528
527
|
appendBufferedText(text);
|
|
529
528
|
appendBufferedUrls(urls);
|
|
530
529
|
return;
|
|
531
530
|
}
|
|
532
|
-
if (!splitFullOutput
|
|
531
|
+
if (!splitFullOutput) {
|
|
533
532
|
appendBufferedText(text);
|
|
534
533
|
appendBufferedUrls(urls);
|
|
535
534
|
return;
|
|
@@ -597,9 +596,6 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
597
596
|
if (outputVisibility === "full") {
|
|
598
597
|
await emitFullSegment(text, urls);
|
|
599
598
|
}
|
|
600
|
-
else if (splitNormalBlockOutput) {
|
|
601
|
-
await emitFullSegment(text, urls);
|
|
602
|
-
}
|
|
603
599
|
else if (outputVisibility === "minimal" || outputVisibility === "normal") {
|
|
604
600
|
appendBufferedText(text);
|
|
605
601
|
appendBufferedUrls(urls);
|
|
@@ -631,12 +627,6 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
631
627
|
}
|
|
632
628
|
const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
|
|
633
629
|
const finalUrls = mergeFinalUrls(urls);
|
|
634
|
-
if (isClawChatNoopResponseText(finalText) &&
|
|
635
|
-
!richFragment &&
|
|
636
|
-
finalUrls.length === 0) {
|
|
637
|
-
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
630
|
const mediaFragments = await uploadMediaUrls(finalUrls);
|
|
641
631
|
const result = await sendStatic(finalText, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
|
|
642
632
|
if (result?.messageId)
|
|
@@ -648,8 +638,12 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
648
638
|
onError: (error, info) => {
|
|
649
639
|
const errorText = normalizeReplyErrorText(error);
|
|
650
640
|
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`);
|
|
651
|
-
if (outputVisibility === "full")
|
|
641
|
+
if (!isGroupTarget && outputVisibility === "full")
|
|
652
642
|
void emitFullRuntimeText("error", errorText);
|
|
643
|
+
if (isGroupTarget) {
|
|
644
|
+
log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
653
647
|
},
|
|
654
648
|
onIdle: async () => {
|
|
655
649
|
emitTyping(false);
|
|
@@ -676,10 +670,10 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
676
670
|
replyOptions: {
|
|
677
671
|
...base.replyOptions,
|
|
678
672
|
sourceReplyDeliveryMode: "automatic",
|
|
679
|
-
disableBlockStreaming:
|
|
673
|
+
disableBlockStreaming: true,
|
|
680
674
|
suppressDefaultToolProgressMessages: true,
|
|
681
|
-
allowProgressCallbacksWhenSourceDeliverySuppressed:
|
|
682
|
-
onReasoningStream:
|
|
675
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: outputVisibility === "full" ? true : undefined,
|
|
676
|
+
onReasoningStream: outputVisibility === "full"
|
|
683
677
|
? async (payload) => {
|
|
684
678
|
if (consumeTerminalSend("reasoning"))
|
|
685
679
|
return;
|
|
@@ -690,14 +684,14 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
690
684
|
reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
691
685
|
}
|
|
692
686
|
: undefined,
|
|
693
|
-
onToolStart:
|
|
687
|
+
onToolStart: outputVisibility === "full"
|
|
694
688
|
? async (payload) => {
|
|
695
689
|
if (consumeTerminalSend("tool-start"))
|
|
696
690
|
return;
|
|
697
691
|
await emitFullSegment(formatToolStartSummary(payload));
|
|
698
692
|
}
|
|
699
693
|
: undefined,
|
|
700
|
-
onToolResult:
|
|
694
|
+
onToolResult: outputVisibility === "full"
|
|
701
695
|
? async (payload) => {
|
|
702
696
|
if (consumeTerminalSend("tool-result"))
|
|
703
697
|
return;
|
|
@@ -707,7 +701,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
707
701
|
await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
708
702
|
}
|
|
709
703
|
: undefined,
|
|
710
|
-
onItemEvent:
|
|
704
|
+
onItemEvent: outputVisibility === "full"
|
|
711
705
|
? async (payload) => {
|
|
712
706
|
if (consumeTerminalSend("item-event"))
|
|
713
707
|
return;
|
|
@@ -716,35 +710,35 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
716
710
|
await emitFullRuntimeText("progress", summarizeProgressPayload(payload));
|
|
717
711
|
}
|
|
718
712
|
: undefined,
|
|
719
|
-
onPlanUpdate:
|
|
713
|
+
onPlanUpdate: outputVisibility === "full"
|
|
720
714
|
? async (payload) => {
|
|
721
715
|
if (consumeTerminalSend("plan-update"))
|
|
722
716
|
return;
|
|
723
717
|
await emitFullRuntimeText("plan", summarizeProgressPayload(payload));
|
|
724
718
|
}
|
|
725
719
|
: undefined,
|
|
726
|
-
onCommandOutput:
|
|
720
|
+
onCommandOutput: outputVisibility === "full"
|
|
727
721
|
? async (payload) => {
|
|
728
722
|
if (consumeTerminalSend("command-output"))
|
|
729
723
|
return;
|
|
730
724
|
await emitFullSegment(formatCommandOutputSummary(payload));
|
|
731
725
|
}
|
|
732
726
|
: undefined,
|
|
733
|
-
onPatchSummary:
|
|
727
|
+
onPatchSummary: outputVisibility === "full"
|
|
734
728
|
? async (payload) => {
|
|
735
729
|
if (consumeTerminalSend("patch-summary"))
|
|
736
730
|
return;
|
|
737
731
|
await emitFullSegment(formatPatchSummary(payload));
|
|
738
732
|
}
|
|
739
733
|
: undefined,
|
|
740
|
-
onCompactionStart:
|
|
734
|
+
onCompactionStart: outputVisibility === "full"
|
|
741
735
|
? async () => {
|
|
742
736
|
if (consumeTerminalSend("compaction-start"))
|
|
743
737
|
return;
|
|
744
738
|
await emitFullSegment("[compaction] started");
|
|
745
739
|
}
|
|
746
740
|
: undefined,
|
|
747
|
-
onCompactionEnd:
|
|
741
|
+
onCompactionEnd: outputVisibility === "full"
|
|
748
742
|
? async () => {
|
|
749
743
|
if (consumeTerminalSend("compaction-end"))
|
|
750
744
|
return;
|
package/dist/src/runtime.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.js";
|
|
|
12
12
|
import { runWithTerminalClawChatSendScope } from "./terminal-send.js";
|
|
13
13
|
import { flushAlignedOutboundQueue, getAlignedOutboundQueueSize, sendOpenclawClawlingText, setAlignedOutboundLogContext, } from "./outbound.js";
|
|
14
14
|
import { formatWsLog } from "./ws-log.js";
|
|
15
|
-
import { createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.js";
|
|
15
|
+
import { createNotifySignalObserver, createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.js";
|
|
16
16
|
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
17
17
|
import { getClawChatGroupPrompt, getClawChatUserPrompt } from "./plugin-prompts.js";
|
|
18
18
|
import { loadClawChatPromptMetadata, renderClawChatProfilePrompt, resolveSenderRelation, } from "./profile-prompt.js";
|
|
@@ -641,6 +641,11 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
641
641
|
send: () => { },
|
|
642
642
|
context: wsLogContext,
|
|
643
643
|
});
|
|
644
|
+
const notifySignalObserver = createNotifySignalObserver({
|
|
645
|
+
accountId,
|
|
646
|
+
log: (msg) => log?.info?.(msg),
|
|
647
|
+
context: wsLogContext,
|
|
648
|
+
});
|
|
644
649
|
const logAuthFailure = (reason) => {
|
|
645
650
|
if (authFailureLogged)
|
|
646
651
|
return;
|
|
@@ -863,6 +868,19 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
863
868
|
client.on("metadata:invalidated", (env) => {
|
|
864
869
|
void handleMetadataInvalidation(env);
|
|
865
870
|
});
|
|
871
|
+
client.on("notify:signal", (env) => {
|
|
872
|
+
// §9.4 reliable system notification. The plugin holds no friend/roster
|
|
873
|
+
// cache (friends are fetched on demand via REST tools), so there is nothing
|
|
874
|
+
// to invalidate — observe + dedup only. The live frame and its reliable
|
|
875
|
+
// inbox replay carry the same event_id and collapse to one observation.
|
|
876
|
+
notifySignalObserver.observe(env);
|
|
877
|
+
});
|
|
878
|
+
client.on("replay:done", (env) => {
|
|
879
|
+
// §11.5 terminal control frame: device replay drained, live delivery begins.
|
|
880
|
+
// Fires on every reconnect (even zero-backlog). Replayed messages are
|
|
881
|
+
// processed inline, so this is a logged boundary marker, not a gate.
|
|
882
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw replay.done trace=${env.trace_id}`);
|
|
883
|
+
});
|
|
866
884
|
client.on("error", (err) => {
|
|
867
885
|
const classified = classifyClawlingClientError(err);
|
|
868
886
|
if (classified.kind === "auth") {
|
package/dist/src/ws-alignment.js
CHANGED
|
@@ -176,3 +176,70 @@ export function createProtocolControlHandler(options) {
|
|
|
176
176
|
},
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Observes reliable `notify.signal` frames (§9.4). The agent plugin keeps no
|
|
181
|
+
* friend/roster cache (friends are fetched on demand via REST tools), so there
|
|
182
|
+
* is nothing to invalidate — this is a pure observability hook: it dedups by
|
|
183
|
+
* `event_id` (the live frame and its reliable-inbox replay collapse to one),
|
|
184
|
+
* structured-logs the signal, and returns the outcome. It deliberately takes no
|
|
185
|
+
* action on the agent; wire a real reaction here if the product later needs one.
|
|
186
|
+
*/
|
|
187
|
+
export function createNotifySignalObserver(options) {
|
|
188
|
+
const maxSeen = options.maxSeen ?? 512;
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const order = [];
|
|
191
|
+
const context = () => options.context?.() ?? { attempt: 1, reconnectCount: 0, state: "ready" };
|
|
192
|
+
const logSignal = (event, action, fields) => {
|
|
193
|
+
const current = context();
|
|
194
|
+
options.log(formatWsLog({
|
|
195
|
+
event,
|
|
196
|
+
accountId: options.accountId,
|
|
197
|
+
attempt: current.attempt,
|
|
198
|
+
reconnectCount: current.reconnectCount,
|
|
199
|
+
state: current.state,
|
|
200
|
+
action,
|
|
201
|
+
fields,
|
|
202
|
+
}));
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
/** Returns whether this signal was newly observed, a duplicate, or malformed. */
|
|
206
|
+
observe(env) {
|
|
207
|
+
const payload = env.payload && typeof env.payload === "object"
|
|
208
|
+
? env.payload
|
|
209
|
+
: undefined;
|
|
210
|
+
const eventId = typeof payload?.event_id === "string" ? payload.event_id : "";
|
|
211
|
+
const type = typeof payload?.type === "string" ? payload.type : "";
|
|
212
|
+
const entityId = typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
213
|
+
const version = typeof payload?.version === "number" ? payload.version : undefined;
|
|
214
|
+
if (!eventId || !type) {
|
|
215
|
+
logSignal("notify_signal_invalid", "ignore", [
|
|
216
|
+
["trace_id", env.trace_id],
|
|
217
|
+
["type", type || null],
|
|
218
|
+
["event_id", eventId || null],
|
|
219
|
+
]);
|
|
220
|
+
return "invalid";
|
|
221
|
+
}
|
|
222
|
+
if (seen.has(eventId)) {
|
|
223
|
+
logSignal("notify_signal_duplicate", "ignore", [
|
|
224
|
+
["type", type],
|
|
225
|
+
["event_id", eventId],
|
|
226
|
+
]);
|
|
227
|
+
return "duplicate";
|
|
228
|
+
}
|
|
229
|
+
seen.add(eventId);
|
|
230
|
+
order.push(eventId);
|
|
231
|
+
while (order.length > maxSeen) {
|
|
232
|
+
const evicted = order.shift();
|
|
233
|
+
if (evicted !== undefined)
|
|
234
|
+
seen.delete(evicted);
|
|
235
|
+
}
|
|
236
|
+
logSignal("notify_signal_observed", "observe", [
|
|
237
|
+
["type", type],
|
|
238
|
+
["entity_id", entityId || null],
|
|
239
|
+
["version", version ?? null],
|
|
240
|
+
["event_id", eventId],
|
|
241
|
+
]);
|
|
242
|
+
return "observed";
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
package/dist/src/ws-client.js
CHANGED
|
@@ -301,6 +301,10 @@ export class ClawChatClient extends EventEmitter {
|
|
|
301
301
|
this.emit("typing", env);
|
|
302
302
|
if (env.event === EVENT.CHAT_METADATA_INVALIDATED)
|
|
303
303
|
this.emit("metadata:invalidated", env);
|
|
304
|
+
if (env.event === EVENT.NOTIFY_SIGNAL)
|
|
305
|
+
this.emit("notify:signal", env);
|
|
306
|
+
if (env.event === EVENT.REPLAY_DONE)
|
|
307
|
+
this.emit("replay:done", env);
|
|
304
308
|
if (env.event === EVENT.OFFLINE_DONE)
|
|
305
309
|
this.emit("offline:done");
|
|
306
310
|
}
|
|
@@ -322,7 +326,15 @@ export class ClawChatClient extends EventEmitter {
|
|
|
322
326
|
token: this.opts.token,
|
|
323
327
|
nonce,
|
|
324
328
|
...(this.opts.deviceId ? { device_id: this.opts.deviceId } : {}),
|
|
325
|
-
|
|
329
|
+
// Agent runtime is single-device: multi_device stays off so the server
|
|
330
|
+
// never self-fans-out this connection's own messages. notify_signals is
|
|
331
|
+
// advertised because we now handle the notify.signal frame (§9.4).
|
|
332
|
+
capabilities: {
|
|
333
|
+
multi_device: false,
|
|
334
|
+
device_replay: true,
|
|
335
|
+
chat_meta_events: true,
|
|
336
|
+
notify_signals: true,
|
|
337
|
+
},
|
|
326
338
|
};
|
|
327
339
|
const traceId = this.nextTraceId();
|
|
328
340
|
this.expectedConnectTraceId = traceId;
|
package/package.json
CHANGED
package/skills/clawchat/SKILL.md
CHANGED
|
@@ -13,7 +13,6 @@ 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.
|
|
17
16
|
|
|
18
17
|
## OpenClaw CLI
|
|
19
18
|
|
|
@@ -31,18 +30,6 @@ Use `update --force` only when local ClawChat plugin or skill files look corrupt
|
|
|
31
30
|
|
|
32
31
|
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.
|
|
33
32
|
|
|
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
|
-
|
|
46
33
|
## Plugin Tool Routing
|
|
47
34
|
|
|
48
35
|
Tool descriptions are authoritative. These routing hints resolve common ambiguity:
|
package/src/protocol-types.ts
CHANGED
|
@@ -13,6 +13,8 @@ export const EVENT = {
|
|
|
13
13
|
MESSAGE_FAILED: "message.failed",
|
|
14
14
|
TYPING_UPDATE: "typing.update",
|
|
15
15
|
CHAT_METADATA_INVALIDATED: "chat.metadata.invalidated",
|
|
16
|
+
NOTIFY_SIGNAL: "notify.signal",
|
|
17
|
+
REPLAY_DONE: "replay.done",
|
|
16
18
|
OFFLINE_BATCH: "offline.batch",
|
|
17
19
|
OFFLINE_ACK: "offline.ack",
|
|
18
20
|
OFFLINE_DONE: "offline.done",
|
|
@@ -78,6 +80,11 @@ export interface ConnectCapabilities {
|
|
|
78
80
|
multi_device?: boolean;
|
|
79
81
|
device_replay?: boolean;
|
|
80
82
|
chat_meta_events?: boolean;
|
|
83
|
+
delivery_receipt?: boolean;
|
|
84
|
+
notify_signals?: boolean;
|
|
85
|
+
permission_events?: boolean;
|
|
86
|
+
history_sync?: boolean;
|
|
87
|
+
e2ee?: boolean;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
export interface ConnectPayload {
|
|
@@ -207,6 +214,25 @@ export interface ChatMetadataInvalidatedPayload {
|
|
|
207
214
|
updated_at?: number;
|
|
208
215
|
}
|
|
209
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Reliable, inbox-coalesced system notification (§9.4). Content-free — only
|
|
219
|
+
* enough identity to dedup and to decide which REST surface to refetch. The
|
|
220
|
+
* agent plugin keeps no friend/roster cache, so this is consumed as an
|
|
221
|
+
* observability signal (see `createNotifySignalObserver`), not a cache refresh.
|
|
222
|
+
*/
|
|
223
|
+
export interface NotifySignalPayload {
|
|
224
|
+
/** Logical event type the client routes on, e.g. `friend.added`. */
|
|
225
|
+
type: string;
|
|
226
|
+
/** Id of the changed entity (meaning depends on `type`). */
|
|
227
|
+
entity_id: string;
|
|
228
|
+
/** Monotonic cursor (ms since epoch at mutation time). */
|
|
229
|
+
version: number;
|
|
230
|
+
/** Globally-unique id for this signal occurrence — cross-channel dedup key. */
|
|
231
|
+
event_id: string;
|
|
232
|
+
/** Inbox coalesce key, formatted `notify:{type}:{entity_id}`. */
|
|
233
|
+
message_id: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
210
236
|
export interface StreamCreatedPayload {
|
|
211
237
|
message_id: string;
|
|
212
238
|
message_mode?: string;
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -388,8 +388,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
388
388
|
} = options;
|
|
389
389
|
const isGroupTarget = target.chatType === "group";
|
|
390
390
|
const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
|
|
391
|
-
const splitFullOutput = outputVisibility === "full";
|
|
392
|
-
const splitNormalBlockOutput = outputVisibility === "normal";
|
|
391
|
+
const splitFullOutput = outputVisibility === "full" && !isGroupTarget;
|
|
393
392
|
const ownerDirectTarget = () => {
|
|
394
393
|
const ownerUserId = account.ownerUserId?.trim();
|
|
395
394
|
return ownerUserId ? { chatId: ownerUserId, chatType: "direct" as const } : null;
|
|
@@ -671,12 +670,12 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
671
670
|
};
|
|
672
671
|
|
|
673
672
|
const emitFullSegment = async (text: string, urls: string[] = []): Promise<void> => {
|
|
674
|
-
if (outputVisibility !== "full"
|
|
673
|
+
if (outputVisibility !== "full") {
|
|
675
674
|
appendBufferedText(text);
|
|
676
675
|
appendBufferedUrls(urls);
|
|
677
676
|
return;
|
|
678
677
|
}
|
|
679
|
-
if (!splitFullOutput
|
|
678
|
+
if (!splitFullOutput) {
|
|
680
679
|
appendBufferedText(text);
|
|
681
680
|
appendBufferedUrls(urls);
|
|
682
681
|
return;
|
|
@@ -751,8 +750,6 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
751
750
|
if (info?.kind === "block") {
|
|
752
751
|
if (outputVisibility === "full") {
|
|
753
752
|
await emitFullSegment(text, urls);
|
|
754
|
-
} else if (splitNormalBlockOutput) {
|
|
755
|
-
await emitFullSegment(text, urls);
|
|
756
753
|
} else if (outputVisibility === "minimal" || outputVisibility === "normal") {
|
|
757
754
|
appendBufferedText(text);
|
|
758
755
|
appendBufferedUrls(urls);
|
|
@@ -785,14 +782,6 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
785
782
|
}
|
|
786
783
|
const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
|
|
787
784
|
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
|
-
}
|
|
796
785
|
const mediaFragments = await uploadMediaUrls(finalUrls);
|
|
797
786
|
const result = await sendStatic(
|
|
798
787
|
finalText,
|
|
@@ -811,7 +800,13 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
811
800
|
log?.error?.(
|
|
812
801
|
`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`,
|
|
813
802
|
);
|
|
814
|
-
if (outputVisibility === "full") void emitFullRuntimeText("error", errorText);
|
|
803
|
+
if (!isGroupTarget && outputVisibility === "full") void emitFullRuntimeText("error", errorText);
|
|
804
|
+
if (isGroupTarget) {
|
|
805
|
+
log?.error?.(
|
|
806
|
+
`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`,
|
|
807
|
+
);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
815
810
|
},
|
|
816
811
|
onIdle: async () => {
|
|
817
812
|
emitTyping(false);
|
|
@@ -835,10 +830,10 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
835
830
|
replyOptions: {
|
|
836
831
|
...base.replyOptions,
|
|
837
832
|
sourceReplyDeliveryMode: "automatic",
|
|
838
|
-
disableBlockStreaming:
|
|
833
|
+
disableBlockStreaming: true,
|
|
839
834
|
suppressDefaultToolProgressMessages: true,
|
|
840
|
-
allowProgressCallbacksWhenSourceDeliverySuppressed:
|
|
841
|
-
onReasoningStream:
|
|
835
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: outputVisibility === "full" ? true : undefined,
|
|
836
|
+
onReasoningStream: outputVisibility === "full"
|
|
842
837
|
? async (payload: ReplyPayload) => {
|
|
843
838
|
if (consumeTerminalSend("reasoning")) return;
|
|
844
839
|
const text = resolvePayloadText(payload);
|
|
@@ -847,13 +842,13 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
847
842
|
if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
848
843
|
}
|
|
849
844
|
: undefined,
|
|
850
|
-
onToolStart:
|
|
845
|
+
onToolStart: outputVisibility === "full"
|
|
851
846
|
? async (payload) => {
|
|
852
847
|
if (consumeTerminalSend("tool-start")) return;
|
|
853
848
|
await emitFullSegment(formatToolStartSummary(payload));
|
|
854
849
|
}
|
|
855
850
|
: undefined,
|
|
856
|
-
onToolResult:
|
|
851
|
+
onToolResult: outputVisibility === "full"
|
|
857
852
|
? async (payload: ReplyPayload) => {
|
|
858
853
|
if (consumeTerminalSend("tool-result")) return;
|
|
859
854
|
const text = resolvePayloadText(payload);
|
|
@@ -861,38 +856,38 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
861
856
|
await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
862
857
|
}
|
|
863
858
|
: undefined,
|
|
864
|
-
onItemEvent:
|
|
859
|
+
onItemEvent: outputVisibility === "full"
|
|
865
860
|
? async (payload: Record<string, unknown>) => {
|
|
866
861
|
if (consumeTerminalSend("item-event")) return;
|
|
867
862
|
if (isToolProgressItem(payload)) return;
|
|
868
863
|
await emitFullRuntimeText("progress", summarizeProgressPayload(payload));
|
|
869
864
|
}
|
|
870
865
|
: undefined,
|
|
871
|
-
onPlanUpdate:
|
|
866
|
+
onPlanUpdate: outputVisibility === "full"
|
|
872
867
|
? async (payload: Record<string, unknown>) => {
|
|
873
868
|
if (consumeTerminalSend("plan-update")) return;
|
|
874
869
|
await emitFullRuntimeText("plan", summarizeProgressPayload(payload));
|
|
875
870
|
}
|
|
876
871
|
: undefined,
|
|
877
|
-
onCommandOutput:
|
|
872
|
+
onCommandOutput: outputVisibility === "full"
|
|
878
873
|
? async (payload: Record<string, unknown>) => {
|
|
879
874
|
if (consumeTerminalSend("command-output")) return;
|
|
880
875
|
await emitFullSegment(formatCommandOutputSummary(payload));
|
|
881
876
|
}
|
|
882
877
|
: undefined,
|
|
883
|
-
onPatchSummary:
|
|
878
|
+
onPatchSummary: outputVisibility === "full"
|
|
884
879
|
? async (payload: Record<string, unknown>) => {
|
|
885
880
|
if (consumeTerminalSend("patch-summary")) return;
|
|
886
881
|
await emitFullSegment(formatPatchSummary(payload));
|
|
887
882
|
}
|
|
888
883
|
: undefined,
|
|
889
|
-
onCompactionStart:
|
|
884
|
+
onCompactionStart: outputVisibility === "full"
|
|
890
885
|
? async () => {
|
|
891
886
|
if (consumeTerminalSend("compaction-start")) return;
|
|
892
887
|
await emitFullSegment("[compaction] started");
|
|
893
888
|
}
|
|
894
889
|
: undefined,
|
|
895
|
-
onCompactionEnd:
|
|
890
|
+
onCompactionEnd: outputVisibility === "full"
|
|
896
891
|
? async () => {
|
|
897
892
|
if (consumeTerminalSend("compaction-end")) return;
|
|
898
893
|
await emitFullSegment("[compaction] finished");
|
package/src/runtime.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
setAlignedOutboundLogContext,
|
|
36
36
|
} from "./outbound.ts";
|
|
37
37
|
import { formatWsLog } from "./ws-log.ts";
|
|
38
|
-
import { createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.ts";
|
|
38
|
+
import { createNotifySignalObserver, createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.ts";
|
|
39
39
|
import {
|
|
40
40
|
clawChatDbPathForStateDir,
|
|
41
41
|
getClawChatStore,
|
|
@@ -837,6 +837,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
837
837
|
send: () => {},
|
|
838
838
|
context: wsLogContext,
|
|
839
839
|
});
|
|
840
|
+
const notifySignalObserver = createNotifySignalObserver({
|
|
841
|
+
accountId,
|
|
842
|
+
log: (msg) => log?.info?.(msg),
|
|
843
|
+
context: wsLogContext,
|
|
844
|
+
});
|
|
840
845
|
const logAuthFailure = (reason: string) => {
|
|
841
846
|
if (authFailureLogged) return;
|
|
842
847
|
authFailureLogged = true;
|
|
@@ -1076,6 +1081,21 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1076
1081
|
void handleMetadataInvalidation(env);
|
|
1077
1082
|
});
|
|
1078
1083
|
|
|
1084
|
+
client.on("notify:signal", (env: Envelope) => {
|
|
1085
|
+
// §9.4 reliable system notification. The plugin holds no friend/roster
|
|
1086
|
+
// cache (friends are fetched on demand via REST tools), so there is nothing
|
|
1087
|
+
// to invalidate — observe + dedup only. The live frame and its reliable
|
|
1088
|
+
// inbox replay carry the same event_id and collapse to one observation.
|
|
1089
|
+
notifySignalObserver.observe(env);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
client.on("replay:done", (env: Envelope) => {
|
|
1093
|
+
// §11.5 terminal control frame: device replay drained, live delivery begins.
|
|
1094
|
+
// Fires on every reconnect (even zero-backlog). Replayed messages are
|
|
1095
|
+
// processed inline, so this is a logged boundary marker, not a gate.
|
|
1096
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw replay.done trace=${env.trace_id}`);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1079
1099
|
client.on("error", (err: unknown) => {
|
|
1080
1100
|
const classified = classifyClawlingClientError(err);
|
|
1081
1101
|
if (classified.kind === "auth") {
|
package/src/ws-alignment.ts
CHANGED
|
@@ -273,3 +273,100 @@ export function createProtocolControlHandler(options: CreateProtocolControlHandl
|
|
|
273
273
|
},
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
|
+
|
|
277
|
+
export interface NotifySignalEnvelope {
|
|
278
|
+
event?: string;
|
|
279
|
+
trace_id?: string;
|
|
280
|
+
payload?: unknown;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface CreateNotifySignalObserverOptions {
|
|
284
|
+
accountId: string;
|
|
285
|
+
log: (msg: string) => void;
|
|
286
|
+
context?: () => WsLogContext;
|
|
287
|
+
/** Upper bound on retained event_ids for dedup (FIFO eviction). */
|
|
288
|
+
maxSeen?: number;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export type NotifySignalOutcome = "observed" | "duplicate" | "invalid";
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Observes reliable `notify.signal` frames (§9.4). The agent plugin keeps no
|
|
295
|
+
* friend/roster cache (friends are fetched on demand via REST tools), so there
|
|
296
|
+
* is nothing to invalidate — this is a pure observability hook: it dedups by
|
|
297
|
+
* `event_id` (the live frame and its reliable-inbox replay collapse to one),
|
|
298
|
+
* structured-logs the signal, and returns the outcome. It deliberately takes no
|
|
299
|
+
* action on the agent; wire a real reaction here if the product later needs one.
|
|
300
|
+
*/
|
|
301
|
+
export function createNotifySignalObserver(options: CreateNotifySignalObserverOptions) {
|
|
302
|
+
const maxSeen = options.maxSeen ?? 512;
|
|
303
|
+
const seen = new Set<string>();
|
|
304
|
+
const order: string[] = [];
|
|
305
|
+
|
|
306
|
+
const context = (): WsLogContext =>
|
|
307
|
+
options.context?.() ?? { attempt: 1, reconnectCount: 0, state: "ready" };
|
|
308
|
+
|
|
309
|
+
const logSignal = (
|
|
310
|
+
event: string,
|
|
311
|
+
action: string,
|
|
312
|
+
fields: Array<[string, string | number | boolean | null | undefined]>,
|
|
313
|
+
) => {
|
|
314
|
+
const current = context();
|
|
315
|
+
options.log(
|
|
316
|
+
formatWsLog({
|
|
317
|
+
event,
|
|
318
|
+
accountId: options.accountId,
|
|
319
|
+
attempt: current.attempt,
|
|
320
|
+
reconnectCount: current.reconnectCount,
|
|
321
|
+
state: current.state,
|
|
322
|
+
action,
|
|
323
|
+
fields,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
/** Returns whether this signal was newly observed, a duplicate, or malformed. */
|
|
330
|
+
observe(env: NotifySignalEnvelope): NotifySignalOutcome {
|
|
331
|
+
const payload = env.payload && typeof env.payload === "object"
|
|
332
|
+
? env.payload as Record<string, unknown>
|
|
333
|
+
: undefined;
|
|
334
|
+
const eventId = typeof payload?.event_id === "string" ? payload.event_id : "";
|
|
335
|
+
const type = typeof payload?.type === "string" ? payload.type : "";
|
|
336
|
+
const entityId = typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
337
|
+
const version = typeof payload?.version === "number" ? payload.version : undefined;
|
|
338
|
+
|
|
339
|
+
if (!eventId || !type) {
|
|
340
|
+
logSignal("notify_signal_invalid", "ignore", [
|
|
341
|
+
["trace_id", env.trace_id],
|
|
342
|
+
["type", type || null],
|
|
343
|
+
["event_id", eventId || null],
|
|
344
|
+
]);
|
|
345
|
+
return "invalid";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (seen.has(eventId)) {
|
|
349
|
+
logSignal("notify_signal_duplicate", "ignore", [
|
|
350
|
+
["type", type],
|
|
351
|
+
["event_id", eventId],
|
|
352
|
+
]);
|
|
353
|
+
return "duplicate";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
seen.add(eventId);
|
|
357
|
+
order.push(eventId);
|
|
358
|
+
while (order.length > maxSeen) {
|
|
359
|
+
const evicted = order.shift();
|
|
360
|
+
if (evicted !== undefined) seen.delete(evicted);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
logSignal("notify_signal_observed", "observe", [
|
|
364
|
+
["type", type],
|
|
365
|
+
["entity_id", entityId || null],
|
|
366
|
+
["version", version ?? null],
|
|
367
|
+
["event_id", eventId],
|
|
368
|
+
]);
|
|
369
|
+
return "observed";
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
package/src/ws-client.ts
CHANGED
|
@@ -369,6 +369,8 @@ export class ClawChatClient extends EventEmitter {
|
|
|
369
369
|
if (env.event === EVENT.MESSAGE_FAILED) this.emit("message:failed", env);
|
|
370
370
|
if (env.event === EVENT.TYPING_UPDATE) this.emit("typing", env);
|
|
371
371
|
if (env.event === EVENT.CHAT_METADATA_INVALIDATED) this.emit("metadata:invalidated", env);
|
|
372
|
+
if (env.event === EVENT.NOTIFY_SIGNAL) this.emit("notify:signal", env);
|
|
373
|
+
if (env.event === EVENT.REPLAY_DONE) this.emit("replay:done", env);
|
|
372
374
|
if (env.event === EVENT.OFFLINE_DONE) this.emit("offline:done");
|
|
373
375
|
}
|
|
374
376
|
|
|
@@ -389,7 +391,15 @@ export class ClawChatClient extends EventEmitter {
|
|
|
389
391
|
token: this.opts.token,
|
|
390
392
|
nonce,
|
|
391
393
|
...(this.opts.deviceId ? { device_id: this.opts.deviceId } : {}),
|
|
392
|
-
|
|
394
|
+
// Agent runtime is single-device: multi_device stays off so the server
|
|
395
|
+
// never self-fans-out this connection's own messages. notify_signals is
|
|
396
|
+
// advertised because we now handle the notify.signal frame (§9.4).
|
|
397
|
+
capabilities: {
|
|
398
|
+
multi_device: false,
|
|
399
|
+
device_replay: true,
|
|
400
|
+
chat_meta_events: true,
|
|
401
|
+
notify_signals: true,
|
|
402
|
+
},
|
|
393
403
|
};
|
|
394
404
|
const traceId = this.nextTraceId();
|
|
395
405
|
this.expectedConnectTraceId = traceId;
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { emitStreamAdd, emitStreamCreated, emitStreamDone, emitStreamFailed, } from "./client.js";
|
|
2
|
-
/**
|
|
3
|
-
* Merge two views of the same progressively-revealed text.
|
|
4
|
-
*
|
|
5
|
-
* The agent runner may give us either:
|
|
6
|
-
* - full snapshots ("Hel", "Hello", "Hello, world") where each item is
|
|
7
|
-
* a superset of the previous; or
|
|
8
|
-
* - overlapping slices ("hello ", "world hello ") that don't share a
|
|
9
|
-
* prefix but share an overlap at the join.
|
|
10
|
-
*
|
|
11
|
-
* This helper returns a longest-sensible combined string. Ported from
|
|
12
|
-
* `clawling-channel/src/reply-dispatcher.ts`.
|
|
13
|
-
*/
|
|
14
|
-
export function mergeStreamingText(previousText, nextText) {
|
|
15
|
-
const currentSnapshot = typeof previousText === "string" ? previousText : "";
|
|
16
|
-
const incomingText = typeof nextText === "string" ? nextText : "";
|
|
17
|
-
if (!incomingText)
|
|
18
|
-
return currentSnapshot;
|
|
19
|
-
if (!currentSnapshot || incomingText === currentSnapshot)
|
|
20
|
-
return incomingText;
|
|
21
|
-
if (incomingText.startsWith(currentSnapshot))
|
|
22
|
-
return incomingText;
|
|
23
|
-
if (currentSnapshot.startsWith(incomingText))
|
|
24
|
-
return currentSnapshot;
|
|
25
|
-
if (incomingText.includes(currentSnapshot))
|
|
26
|
-
return incomingText;
|
|
27
|
-
if (currentSnapshot.includes(incomingText))
|
|
28
|
-
return currentSnapshot;
|
|
29
|
-
const maxOverlap = Math.min(currentSnapshot.length, incomingText.length);
|
|
30
|
-
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
|
|
31
|
-
if (currentSnapshot.slice(-overlap) === incomingText.slice(0, overlap)) {
|
|
32
|
-
return `${currentSnapshot}${incomingText.slice(overlap)}`;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return `${currentSnapshot}${incomingText}`;
|
|
36
|
-
}
|
|
37
|
-
function resolveRouting(options) {
|
|
38
|
-
if (options.routing)
|
|
39
|
-
return options.routing;
|
|
40
|
-
if (options.to)
|
|
41
|
-
return { chatId: options.to.id, chatType: options.to.type };
|
|
42
|
-
throw new Error("openclaw-clawchat buffered stream requires routing");
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Build a streaming session wrapper around message.created/add/done events.
|
|
46
|
-
*
|
|
47
|
-
* Usage pattern (matching clawling-channel):
|
|
48
|
-
* const session = openBufferedStreamingSession({...});
|
|
49
|
-
* await session.queueSnapshot("Hel");
|
|
50
|
-
* await session.queueSnapshot("Hello");
|
|
51
|
-
* await session.queueDelta(", world");
|
|
52
|
-
* await session.done();
|
|
53
|
-
*/
|
|
54
|
-
export function openBufferedStreamingSession(options) {
|
|
55
|
-
const routing = resolveRouting(options);
|
|
56
|
-
const emitTyping = options.emitTyping !== false;
|
|
57
|
-
if (emitTyping)
|
|
58
|
-
options.client.typing(routing.chatId, true);
|
|
59
|
-
emitStreamCreated(options.client, {
|
|
60
|
-
messageId: options.messageId,
|
|
61
|
-
routing,
|
|
62
|
-
});
|
|
63
|
-
let bufferedSnapshot = "";
|
|
64
|
-
let flushedSnapshot = "";
|
|
65
|
-
let sequence = -1;
|
|
66
|
-
let flushTimer = null;
|
|
67
|
-
let pendingFlush = Promise.resolve();
|
|
68
|
-
let closed = false;
|
|
69
|
-
const clearTimer = () => {
|
|
70
|
-
if (flushTimer) {
|
|
71
|
-
clearTimeout(flushTimer);
|
|
72
|
-
flushTimer = null;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
const performFlush = async () => {
|
|
76
|
-
clearTimer();
|
|
77
|
-
if (closed)
|
|
78
|
-
return;
|
|
79
|
-
if (bufferedSnapshot === flushedSnapshot)
|
|
80
|
-
return;
|
|
81
|
-
const snapshot = bufferedSnapshot;
|
|
82
|
-
const delta = snapshot.slice(flushedSnapshot.length);
|
|
83
|
-
if (!delta)
|
|
84
|
-
return;
|
|
85
|
-
sequence += 1;
|
|
86
|
-
emitStreamAdd(options.client, {
|
|
87
|
-
messageId: options.messageId,
|
|
88
|
-
routing,
|
|
89
|
-
sequence,
|
|
90
|
-
fullText: snapshot,
|
|
91
|
-
textDelta: delta,
|
|
92
|
-
});
|
|
93
|
-
flushedSnapshot = snapshot;
|
|
94
|
-
};
|
|
95
|
-
const flush = async () => {
|
|
96
|
-
pendingFlush = pendingFlush.then(performFlush);
|
|
97
|
-
await pendingFlush;
|
|
98
|
-
};
|
|
99
|
-
const scheduleFlush = () => {
|
|
100
|
-
if (flushTimer || closed)
|
|
101
|
-
return;
|
|
102
|
-
flushTimer = setTimeout(() => {
|
|
103
|
-
flushTimer = null;
|
|
104
|
-
void flush();
|
|
105
|
-
}, options.flushIntervalMs);
|
|
106
|
-
};
|
|
107
|
-
const queueSnapshot = async (snapshot) => {
|
|
108
|
-
if (closed || !snapshot)
|
|
109
|
-
return;
|
|
110
|
-
const base = bufferedSnapshot.length >= flushedSnapshot.length ? bufferedSnapshot : flushedSnapshot;
|
|
111
|
-
const merged = mergeStreamingText(base, snapshot);
|
|
112
|
-
if (merged === bufferedSnapshot)
|
|
113
|
-
return;
|
|
114
|
-
bufferedSnapshot = merged;
|
|
115
|
-
const deltaChars = Math.max(0, bufferedSnapshot.length - flushedSnapshot.length);
|
|
116
|
-
if (deltaChars >= options.minChunkChars || bufferedSnapshot.length >= options.maxBufferChars) {
|
|
117
|
-
await flush();
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
scheduleFlush();
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
const queueDelta = async (delta) => {
|
|
124
|
-
if (closed || !delta)
|
|
125
|
-
return;
|
|
126
|
-
bufferedSnapshot = `${bufferedSnapshot}${delta}`;
|
|
127
|
-
const deltaChars = Math.max(0, bufferedSnapshot.length - flushedSnapshot.length);
|
|
128
|
-
if (deltaChars >= options.minChunkChars || bufferedSnapshot.length >= options.maxBufferChars) {
|
|
129
|
-
await flush();
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
scheduleFlush();
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
const done = async () => {
|
|
136
|
-
if (closed)
|
|
137
|
-
return;
|
|
138
|
-
await flush();
|
|
139
|
-
closed = true;
|
|
140
|
-
clearTimer();
|
|
141
|
-
emitStreamDone(options.client, {
|
|
142
|
-
messageId: options.messageId,
|
|
143
|
-
routing,
|
|
144
|
-
finalSequence: Math.max(sequence, 0),
|
|
145
|
-
finalText: bufferedSnapshot,
|
|
146
|
-
});
|
|
147
|
-
if (emitTyping)
|
|
148
|
-
options.client.typing(routing.chatId, false);
|
|
149
|
-
};
|
|
150
|
-
const fail = async (reason) => {
|
|
151
|
-
if (closed)
|
|
152
|
-
return;
|
|
153
|
-
closed = true;
|
|
154
|
-
clearTimer();
|
|
155
|
-
emitStreamFailed(options.client, {
|
|
156
|
-
messageId: options.messageId,
|
|
157
|
-
routing,
|
|
158
|
-
sequence: Math.max(sequence, 0),
|
|
159
|
-
...(reason !== undefined ? { reason } : {}),
|
|
160
|
-
});
|
|
161
|
-
if (emitTyping)
|
|
162
|
-
options.client.typing(routing.chatId, false);
|
|
163
|
-
};
|
|
164
|
-
return {
|
|
165
|
-
get currentText() {
|
|
166
|
-
return bufferedSnapshot;
|
|
167
|
-
},
|
|
168
|
-
get flushedText() {
|
|
169
|
-
return flushedSnapshot;
|
|
170
|
-
},
|
|
171
|
-
queueSnapshot,
|
|
172
|
-
queueDelta,
|
|
173
|
-
flush,
|
|
174
|
-
done,
|
|
175
|
-
fail,
|
|
176
|
-
};
|
|
177
|
-
}
|
package/dist/src/streaming.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|