@clawling/clawchat-plugin-openclaw 2026.5.12-38 → 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/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/src/protocol-types.ts +26 -0
- 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",
|
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/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/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
|
-
}
|