openclaw-lark-multi-agent 0.1.13 → 0.1.15
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/discussion-manager.d.ts +4 -1
- package/dist/discussion-manager.js +18 -1
- package/dist/feishu-bot.d.ts +10 -0
- package/dist/feishu-bot.js +155 -53
- package/dist/message-store.d.ts +26 -0
- package/dist/message-store.js +165 -0
- package/dist/openclaw-client.d.ts +8 -1
- package/dist/openclaw-client.js +90 -42
- package/package.json +1 -1
|
@@ -20,7 +20,10 @@ type DiscussionSession = {
|
|
|
20
20
|
};
|
|
21
21
|
export type DiscussionParticipant = {
|
|
22
22
|
name: string;
|
|
23
|
-
runDiscussionTurn(chatId: string, prompt: string
|
|
23
|
+
runDiscussionTurn(chatId: string, prompt: string, meta?: {
|
|
24
|
+
round: number;
|
|
25
|
+
maxRounds: number;
|
|
26
|
+
}): Promise<ReplyResult>;
|
|
24
27
|
};
|
|
25
28
|
export declare class DiscussionManager {
|
|
26
29
|
private sessions;
|
|
@@ -70,7 +70,7 @@ export class DiscussionManager {
|
|
|
70
70
|
const prompt = this.buildPrompt(session);
|
|
71
71
|
const results = await Promise.allSettled(participants.map(async (participant) => {
|
|
72
72
|
try {
|
|
73
|
-
return await participant.runDiscussionTurn(session.chatId, prompt);
|
|
73
|
+
return await participant.runDiscussionTurn(session.chatId, prompt, { round: session.currentRound, maxRounds: session.maxRounds });
|
|
74
74
|
}
|
|
75
75
|
catch (err) {
|
|
76
76
|
return {
|
|
@@ -92,10 +92,27 @@ export class DiscussionManager {
|
|
|
92
92
|
replies[value.botName] = value.error ? `[ERROR] ${value.error}` : value.text.trim();
|
|
93
93
|
}
|
|
94
94
|
current.completedRounds.push({ round: current.currentRound, replies });
|
|
95
|
+
const noReplyNames = participants
|
|
96
|
+
.map((participant) => participant.name)
|
|
97
|
+
.filter((name) => {
|
|
98
|
+
const text = (replies[name] || "").trim();
|
|
99
|
+
return !text || text.toUpperCase() === "NO_REPLY";
|
|
100
|
+
});
|
|
101
|
+
const errorNames = participants
|
|
102
|
+
.map((participant) => participant.name)
|
|
103
|
+
.filter((name) => (replies[name] || "").trim().startsWith("[ERROR]"));
|
|
95
104
|
const allNoReply = participants.every((participant) => {
|
|
96
105
|
const text = (replies[participant.name] || "").trim();
|
|
97
106
|
return !text || text.toUpperCase() === "NO_REPLY" || text.startsWith("[ERROR]");
|
|
98
107
|
});
|
|
108
|
+
if (sendSystemMessage && !allNoReply && (noReplyNames.length > 0 || errorNames.length > 0)) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (noReplyNames.length > 0)
|
|
111
|
+
parts.push(`${noReplyNames.join("、")} 无新增回复`);
|
|
112
|
+
if (errorNames.length > 0)
|
|
113
|
+
parts.push(`${errorNames.join("、")} 出错`);
|
|
114
|
+
await sendSystemMessage(`💬 第 ${current.currentRound}/${current.maxRounds} 轮:${parts.join(";")}`).catch(() => { });
|
|
115
|
+
}
|
|
99
116
|
if (allNoReply) {
|
|
100
117
|
current.status = "completed";
|
|
101
118
|
this.sessions.delete(current.chatId);
|
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export declare class FeishuBot {
|
|
|
29
29
|
private queueRuns;
|
|
30
30
|
/** Per-chat serial send queue to guarantee message order */
|
|
31
31
|
private sendQueue;
|
|
32
|
+
/** Per-chat delayed runtime failure notifications, canceled if a real reply arrives. */
|
|
33
|
+
private delayedFailureTimers;
|
|
32
34
|
private adminOpenId;
|
|
33
35
|
private static allBots;
|
|
34
36
|
constructor(config: BotConfig, openclawClient: OpenClawClient, store: MessageStore, adminOpenId?: string);
|
|
@@ -77,6 +79,14 @@ export declare class FeishuBot {
|
|
|
77
79
|
private stripLeadingCommandMentions;
|
|
78
80
|
private buildMarkdownCard;
|
|
79
81
|
private formatUserVisibleError;
|
|
82
|
+
private isRuntimeFailureText;
|
|
83
|
+
private cancelDelayedFailure;
|
|
84
|
+
private scheduleDelayedFailure;
|
|
85
|
+
private deliverySessionKey;
|
|
86
|
+
private stableHash;
|
|
87
|
+
private deliverySourceId;
|
|
88
|
+
private enqueueAndDispatchDelivery;
|
|
89
|
+
private dispatchPendingDeliveries;
|
|
80
90
|
private isDiscussionCoordinator;
|
|
81
91
|
private getDiscussionParticipants;
|
|
82
92
|
private runDiscussionTurn;
|
package/dist/feishu-bot.js
CHANGED
|
@@ -34,6 +34,8 @@ export class FeishuBot {
|
|
|
34
34
|
queueRuns = new Map();
|
|
35
35
|
/** Per-chat serial send queue to guarantee message order */
|
|
36
36
|
sendQueue = new Map();
|
|
37
|
+
/** Per-chat delayed runtime failure notifications, canceled if a real reply arrives. */
|
|
38
|
+
delayedFailureTimers = new Map();
|
|
37
39
|
adminOpenId;
|
|
38
40
|
static allBots = new Map();
|
|
39
41
|
constructor(config, openclawClient, store, adminOpenId) {
|
|
@@ -114,11 +116,9 @@ export class FeishuBot {
|
|
|
114
116
|
try {
|
|
115
117
|
console.log(`[${this.config.name}] Proactive message for ${chatId.slice(-8)}`);
|
|
116
118
|
const parsed = this.extractBridgeAttachments(text);
|
|
117
|
-
if (parsed.text.trim())
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await this.sendBridgeAttachment(chatId, attachment);
|
|
121
|
-
}
|
|
119
|
+
if (parsed.text.trim() || parsed.attachments.length > 0)
|
|
120
|
+
this.cancelDelayedFailure(chatId);
|
|
121
|
+
await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("proactive", `${Date.now()}:${Math.random()}:${parsed.text.trim()}|${JSON.stringify(parsed.attachments)}`), parsed.text.trim(), parsed.attachments);
|
|
122
122
|
}
|
|
123
123
|
catch (err) {
|
|
124
124
|
console.error(`[${this.config.name}] Failed to deliver proactive msg:`, err.message);
|
|
@@ -503,6 +503,8 @@ export class FeishuBot {
|
|
|
503
503
|
}
|
|
504
504
|
// --- Discuss mode: group-level multi-bot round scheduler. It takes over
|
|
505
505
|
// plain human messages so normal Free mode does not duplicate Round 1.
|
|
506
|
+
// Targeted mentions must fall through to normal routing so @GPT still
|
|
507
|
+
// works while discuss mode is enabled.
|
|
506
508
|
if (chatType !== "p2p" && !isBot && this.store.getChatInfo(chatId)?.discuss) {
|
|
507
509
|
const mentions = message.mentions || [];
|
|
508
510
|
const hasTargetedMention = mentions.some((m) => !this.isAllMentionItem(m));
|
|
@@ -661,6 +663,7 @@ export class FeishuBot {
|
|
|
661
663
|
this.store.markSynced(this.config.name, chatId, maxId);
|
|
662
664
|
this.store.clearPendingTriggers(this.config.name, chatId, maxId);
|
|
663
665
|
const shouldReply = trimmedReply.length > 0 && !explicitNoReply;
|
|
666
|
+
const isRuntimeFailure = shouldReply && this.isRuntimeFailureText(trimmedReply);
|
|
664
667
|
// Record bot reply only if it is user-visible/context-worthy.
|
|
665
668
|
if (shouldReply || hasAttachments) {
|
|
666
669
|
const storedContent = [visibleReply, ...parsedReply.attachments.map((a) => `[Attachment: ${a.type || "file"} ${a.path}]`)]
|
|
@@ -692,32 +695,19 @@ export class FeishuBot {
|
|
|
692
695
|
}
|
|
693
696
|
// Reply to the last human message on Feishu (ordered after tool msgs)
|
|
694
697
|
// Skip empty replies and explicit NO_REPLY responses
|
|
695
|
-
if ((shouldReply || hasAttachments) && lastHuman.messageId) {
|
|
698
|
+
if ((shouldReply || hasAttachments) && lastHuman.messageId && !isRuntimeFailure) {
|
|
696
699
|
if (triggerId && this.store.hasDeliveredReply(this.config.name, chatId, triggerId)) {
|
|
697
700
|
console.warn(`[${this.config.name}] Reply already delivered, skip duplicate for ${chatId.slice(-8)} msgId=${triggerId}`);
|
|
698
701
|
}
|
|
699
702
|
else {
|
|
700
|
-
await this.
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
await this.replyMessage(lastHuman.messageId, visibleReply);
|
|
704
|
-
}
|
|
705
|
-
catch (err) {
|
|
706
|
-
// Reply can fail for historical messages sent before this bot joined the chat
|
|
707
|
-
// (Feishu 230002: Bot/User can NOT be out of the chat). Fall back to a normal
|
|
708
|
-
// chat message so queue processing still completes.
|
|
709
|
-
console.warn(`[${this.config.name}] replyMessage failed, fallback to sendMessage:`, err.message);
|
|
710
|
-
await this.sendMessage(chatId, visibleReply);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
for (const attachment of parsedReply.attachments) {
|
|
714
|
-
await this.sendBridgeAttachment(chatId, attachment);
|
|
715
|
-
}
|
|
716
|
-
if (triggerId)
|
|
717
|
-
this.store.markDeliveredReply(this.config.name, chatId, triggerId, lastHuman.messageId);
|
|
718
|
-
});
|
|
703
|
+
await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("visible", `${(shouldReply ? visibleReply : "").trim()}|${JSON.stringify(parsedReply.attachments)}`), shouldReply ? visibleReply : "", parsedReply.attachments, lastHuman.messageId, `trigger:${triggerId}`);
|
|
704
|
+
if (triggerId)
|
|
705
|
+
this.store.markDeliveredReply(this.config.name, chatId, triggerId, lastHuman.messageId);
|
|
719
706
|
}
|
|
720
707
|
}
|
|
708
|
+
if (isRuntimeFailure && lastHuman.messageId) {
|
|
709
|
+
this.scheduleDelayedFailure(chatId, lastHuman.messageId, visibleReply, triggerId);
|
|
710
|
+
}
|
|
721
711
|
console.log(`[${this.config.name}] [${new Date().toISOString()}] ${shouldReply || hasAttachments ? 'Replied' : 'Skipped (empty/NO_REPLY)'} (${reply.length} chars, attachments=${parsedReply.attachments.length})`);
|
|
722
712
|
// Replace ack reactions with DONE only for trigger messages actually
|
|
723
713
|
// processed in this run. Queued messages that arrived mid-run keep their
|
|
@@ -739,16 +729,12 @@ export class FeishuBot {
|
|
|
739
729
|
console.error(`[${this.config.name}] processQueue error:`, err);
|
|
740
730
|
const errorText = this.formatUserVisibleError(err);
|
|
741
731
|
if (lastHuman.messageId) {
|
|
742
|
-
await this.
|
|
743
|
-
|
|
744
|
-
await this.replyMessage(lastHuman.messageId, errorText);
|
|
745
|
-
}
|
|
746
|
-
catch {
|
|
747
|
-
await this.sendMessage(chatId, errorText);
|
|
748
|
-
}
|
|
732
|
+
await this.enqueueAndDispatchDelivery(chatId, "provider_error", `trigger:${triggerId}:provider-error`, errorText, [], lastHuman.messageId, `trigger:${triggerId}:provider-error`)
|
|
733
|
+
.then(() => {
|
|
749
734
|
if (triggerId)
|
|
750
735
|
this.store.markDeliveredReply(this.config.name, chatId, triggerId, lastHuman.messageId);
|
|
751
|
-
})
|
|
736
|
+
})
|
|
737
|
+
.catch(() => { });
|
|
752
738
|
}
|
|
753
739
|
if (triggerId) {
|
|
754
740
|
// The run failed after OpenClaw/provider rejected it. Notify the user
|
|
@@ -899,6 +885,110 @@ export class FeishuBot {
|
|
|
899
885
|
}
|
|
900
886
|
return `⚠️ ${this.config.name} 这次没有完成回复。\n原因:${reason}`;
|
|
901
887
|
}
|
|
888
|
+
isRuntimeFailureText(text) {
|
|
889
|
+
return text.startsWith("⚠️ Agent 未正常完成") || /\n原因:\s*rpc\b/.test(text);
|
|
890
|
+
}
|
|
891
|
+
cancelDelayedFailure(chatId) {
|
|
892
|
+
const timer = this.delayedFailureTimers.get(chatId);
|
|
893
|
+
if (timer)
|
|
894
|
+
clearTimeout(timer);
|
|
895
|
+
this.delayedFailureTimers.delete(chatId);
|
|
896
|
+
}
|
|
897
|
+
scheduleDelayedFailure(chatId, replyToMessageId, text, triggerId) {
|
|
898
|
+
this.cancelDelayedFailure(chatId);
|
|
899
|
+
const timer = setTimeout(() => {
|
|
900
|
+
this.delayedFailureTimers.delete(chatId);
|
|
901
|
+
void this.enqueueAndDispatchDelivery(chatId, "delayed_error", `trigger:${triggerId}:delayed-error`, text, [], replyToMessageId, `trigger:${triggerId}:delayed-error`)
|
|
902
|
+
.then(() => {
|
|
903
|
+
if (triggerId)
|
|
904
|
+
this.store.markDeliveredReply(this.config.name, chatId, triggerId, replyToMessageId);
|
|
905
|
+
})
|
|
906
|
+
.catch((err) => {
|
|
907
|
+
console.warn(`[${this.config.name}] delayed failure delivery failed:`, err.message);
|
|
908
|
+
});
|
|
909
|
+
}, 60_000);
|
|
910
|
+
this.delayedFailureTimers.set(chatId, timer);
|
|
911
|
+
}
|
|
912
|
+
deliverySessionKey(chatId) {
|
|
913
|
+
return this.getSessionKey(chatId);
|
|
914
|
+
}
|
|
915
|
+
stableHash(text) {
|
|
916
|
+
let hash = 2166136261;
|
|
917
|
+
for (let i = 0; i < text.length; i++) {
|
|
918
|
+
hash ^= text.charCodeAt(i);
|
|
919
|
+
hash = Math.imul(hash, 16777619);
|
|
920
|
+
}
|
|
921
|
+
return (hash >>> 0).toString(36);
|
|
922
|
+
}
|
|
923
|
+
deliverySourceId(kind, text) {
|
|
924
|
+
return `${kind}:${this.stableHash(text)}`;
|
|
925
|
+
}
|
|
926
|
+
async enqueueAndDispatchDelivery(chatId, sourceType, sourceId, text, attachments = [], replyToMessageId, deliveryKey) {
|
|
927
|
+
if (!text.trim() && attachments.length === 0)
|
|
928
|
+
return;
|
|
929
|
+
const attachmentsJson = JSON.stringify(attachments);
|
|
930
|
+
const normalizedPayload = `${text.trim()}|${attachmentsJson}`;
|
|
931
|
+
const contentHash = this.stableHash(normalizedPayload);
|
|
932
|
+
const finalDeliveryKey = deliveryKey || sourceId;
|
|
933
|
+
if (!deliveryKey) {
|
|
934
|
+
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000))
|
|
935
|
+
return;
|
|
936
|
+
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8))
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const deliveryId = this.store.enqueueDelivery({
|
|
940
|
+
sessionKey: this.deliverySessionKey(chatId),
|
|
941
|
+
chatId,
|
|
942
|
+
botName: this.config.name,
|
|
943
|
+
sourceType,
|
|
944
|
+
sourceId,
|
|
945
|
+
deliveryKey: finalDeliveryKey,
|
|
946
|
+
contentHash,
|
|
947
|
+
content: text,
|
|
948
|
+
attachmentsJson,
|
|
949
|
+
replyToMessageId: replyToMessageId || "",
|
|
950
|
+
});
|
|
951
|
+
if (deliveryId === null)
|
|
952
|
+
return;
|
|
953
|
+
await this.dispatchPendingDeliveries(chatId, replyToMessageId);
|
|
954
|
+
}
|
|
955
|
+
async dispatchPendingDeliveries(chatId, replyToMessageId) {
|
|
956
|
+
const pending = this.store.getPendingDeliveries(chatId, this.config.name, 50);
|
|
957
|
+
for (const item of pending) {
|
|
958
|
+
await this.sendOrdered(chatId, async () => {
|
|
959
|
+
try {
|
|
960
|
+
if (!item.id || !this.store.claimDelivery(item.id))
|
|
961
|
+
return;
|
|
962
|
+
const attachments = JSON.parse(item.attachmentsJson || "[]");
|
|
963
|
+
if (item.content.trim()) {
|
|
964
|
+
const replyTarget = item.replyToMessageId || replyToMessageId;
|
|
965
|
+
const shouldReplyToSource = replyTarget && (item.sourceType === "assistant_visible" || item.sourceType === "provider_error" || item.sourceType === "delayed_error");
|
|
966
|
+
if (shouldReplyToSource) {
|
|
967
|
+
try {
|
|
968
|
+
await this.replyMessage(replyTarget, item.content);
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
console.warn(`[${this.config.name}] replyMessage failed, fallback to sendMessage:`, err.message);
|
|
972
|
+
await this.sendMessage(chatId, item.content);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
await this.sendMessage(chatId, item.content);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const attachment of attachments)
|
|
980
|
+
await this.sendBridgeAttachment(chatId, attachment);
|
|
981
|
+
if (item.id)
|
|
982
|
+
this.store.markDeliveryDelivered(item.id);
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
if (item.id)
|
|
986
|
+
this.store.markDeliveryFailed(item.id);
|
|
987
|
+
throw err;
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
902
992
|
isDiscussionCoordinator() {
|
|
903
993
|
const bots = Array.from(FeishuBot.allBots.values()).filter((bot) => bot.store === this.store);
|
|
904
994
|
if (bots.length === 0)
|
|
@@ -910,24 +1000,41 @@ export class FeishuBot {
|
|
|
910
1000
|
.filter((bot) => bot.store === this.store && bot.store.getBotMode(bot.config.name, chatId) === "free")
|
|
911
1001
|
.map((bot) => ({
|
|
912
1002
|
name: bot.config.name,
|
|
913
|
-
runDiscussionTurn: async (_chatId, prompt) => bot.runDiscussionTurn(chatId, prompt),
|
|
1003
|
+
runDiscussionTurn: async (_chatId, prompt, meta) => bot.runDiscussionTurn(chatId, prompt, meta),
|
|
914
1004
|
}));
|
|
915
1005
|
}
|
|
916
|
-
async runDiscussionTurn(chatId, prompt) {
|
|
1006
|
+
async runDiscussionTurn(chatId, prompt, meta) {
|
|
917
1007
|
const sessionKey = await this.ensureSession(chatId);
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1008
|
+
const releaseProactiveMute = this.openclawClient.muteProactiveDelivery(sessionKey);
|
|
1009
|
+
let reply;
|
|
1010
|
+
try {
|
|
1011
|
+
reply = await this.openclawClient.chatSendWithContext({
|
|
1012
|
+
sessionKey,
|
|
1013
|
+
unsyncedMessages: [],
|
|
1014
|
+
currentMessage: prompt,
|
|
1015
|
+
currentSenderName: "Discussion Scheduler",
|
|
1016
|
+
deliver: false,
|
|
1017
|
+
timeoutMs: 1_800_000,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
finally {
|
|
1021
|
+
// OpenClaw can emit the final assistant session.message shortly after
|
|
1022
|
+
// chatSend/collectReply returns. Keep discussion proactive muted briefly;
|
|
1023
|
+
// the discussion coordinator already owns user-visible delivery.
|
|
1024
|
+
releaseProactiveMute(120_000);
|
|
1025
|
+
}
|
|
926
1026
|
const parsedReply = this.extractBridgeAttachments(reply);
|
|
927
|
-
const
|
|
928
|
-
const
|
|
1027
|
+
const rawVisibleReply = parsedReply.text.trim();
|
|
1028
|
+
const discussionMarkerPattern = /\n*—— 第 \d+\/\d+ 轮 · .+$/;
|
|
1029
|
+
const cleanVisibleReply = rawVisibleReply.replace(discussionMarkerPattern, "").trim();
|
|
1030
|
+
let displayReply = cleanVisibleReply;
|
|
1031
|
+
const isVisible = cleanVisibleReply.length > 0 && cleanVisibleReply.toUpperCase() !== "NO_REPLY";
|
|
1032
|
+
if (isVisible && meta) {
|
|
1033
|
+
const roundMarker = `—— 第 ${meta.round}/${meta.maxRounds} 轮 · ${this.config.name}`;
|
|
1034
|
+
displayReply = `${displayReply}\n\n${roundMarker}`;
|
|
1035
|
+
}
|
|
929
1036
|
if (isVisible || parsedReply.attachments.length > 0) {
|
|
930
|
-
const storedContent = [
|
|
1037
|
+
const storedContent = [displayReply, ...parsedReply.attachments.map((a) => `[Attachment: ${a.type || "file"} ${a.path}]`)]
|
|
931
1038
|
.filter(Boolean)
|
|
932
1039
|
.join("\n");
|
|
933
1040
|
this.store.insert({
|
|
@@ -938,14 +1045,9 @@ export class FeishuBot {
|
|
|
938
1045
|
content: storedContent,
|
|
939
1046
|
timestamp: Date.now(),
|
|
940
1047
|
});
|
|
941
|
-
await this.
|
|
942
|
-
if (isVisible)
|
|
943
|
-
await this.sendMessage(chatId, visibleReply);
|
|
944
|
-
for (const attachment of parsedReply.attachments)
|
|
945
|
-
await this.sendBridgeAttachment(chatId, attachment);
|
|
946
|
-
});
|
|
1048
|
+
await this.enqueueAndDispatchDelivery(chatId, "discussion", `discussion:${Date.now()}:${Math.random().toString(36).slice(2)}`, isVisible ? displayReply : "", parsedReply.attachments);
|
|
947
1049
|
}
|
|
948
|
-
return { botName: this.config.name, text:
|
|
1050
|
+
return { botName: this.config.name, text: cleanVisibleReply, visible: isVisible };
|
|
949
1051
|
}
|
|
950
1052
|
async replyMessage(messageId, text) {
|
|
951
1053
|
// Use Feishu CardKit v2 markdown component for full Markdown rendering.
|
package/dist/message-store.d.ts
CHANGED
|
@@ -25,6 +25,23 @@ export interface ChatMessage {
|
|
|
25
25
|
content: string;
|
|
26
26
|
timestamp: number;
|
|
27
27
|
}
|
|
28
|
+
export interface DeliveryOutboxItem {
|
|
29
|
+
id?: number;
|
|
30
|
+
sessionKey: string;
|
|
31
|
+
chatId: string;
|
|
32
|
+
botName: string;
|
|
33
|
+
sourceType: string;
|
|
34
|
+
sourceId: string;
|
|
35
|
+
deliveryKey: string;
|
|
36
|
+
contentHash: string;
|
|
37
|
+
content: string;
|
|
38
|
+
attachmentsJson: string;
|
|
39
|
+
replyToMessageId: string;
|
|
40
|
+
status: "pending" | "delivering" | "delivered" | "failed";
|
|
41
|
+
attempts: number;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
updatedAt: number;
|
|
44
|
+
}
|
|
28
45
|
export declare class MessageStore {
|
|
29
46
|
private db;
|
|
30
47
|
constructor(dbPath?: string);
|
|
@@ -40,6 +57,15 @@ export declare class MessageStore {
|
|
|
40
57
|
clearPendingTrigger(botName: string, chatId: string, messageRowId: number): void;
|
|
41
58
|
hasDeliveredReply(botName: string, chatId: string, triggerMessageRowId: number): boolean;
|
|
42
59
|
markDeliveredReply(botName: string, chatId: string, triggerMessageRowId: number, replyMessageId?: string): void;
|
|
60
|
+
enqueueDelivery(item: Omit<DeliveryOutboxItem, "id" | "status" | "attempts" | "createdAt" | "updatedAt">): number | null;
|
|
61
|
+
getDeliveryBySource(sessionKey: string, sourceType: string, sourceId: string): DeliveryOutboxItem | null;
|
|
62
|
+
getPendingDeliveries(chatId?: string, botName?: string, maxCount?: number): DeliveryOutboxItem[];
|
|
63
|
+
hasRecentSimilarDelivery(botName: string, chatId: string, contentHash: string, windowMs: number): boolean;
|
|
64
|
+
hasRecentOverlappingDelivery(botName: string, chatId: string, content: string, attachmentsJson: string, windowMs: number, minShortLength?: number): boolean;
|
|
65
|
+
claimDelivery(id: number): boolean;
|
|
66
|
+
markDeliveryDelivered(id: number): void;
|
|
67
|
+
markDeliveryFailed(id: number): void;
|
|
68
|
+
private mapDelivery;
|
|
43
69
|
/**
|
|
44
70
|
* Get messages that haven't been synced to a bot's session yet.
|
|
45
71
|
* Returns messages ordered by timestamp ascending.
|
package/dist/message-store.js
CHANGED
|
@@ -82,7 +82,74 @@ export class MessageStore {
|
|
|
82
82
|
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
83
83
|
PRIMARY KEY (bot_name, chat_id)
|
|
84
84
|
);
|
|
85
|
+
|
|
86
|
+
-- Durable delivery ledger for user-visible assistant outputs. All final
|
|
87
|
+
-- text/attachment outputs should be inserted here first and dispatched
|
|
88
|
+
-- once, regardless of whether they came from chat final, proactive
|
|
89
|
+
-- session.message, subagent announce, or delayed error handling.
|
|
90
|
+
CREATE TABLE IF NOT EXISTS delivery_outbox (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
session_key TEXT NOT NULL,
|
|
93
|
+
chat_id TEXT NOT NULL,
|
|
94
|
+
bot_name TEXT NOT NULL,
|
|
95
|
+
source_type TEXT NOT NULL,
|
|
96
|
+
source_id TEXT NOT NULL,
|
|
97
|
+
delivery_key TEXT NOT NULL DEFAULT '',
|
|
98
|
+
content_hash TEXT NOT NULL DEFAULT '',
|
|
99
|
+
content TEXT NOT NULL DEFAULT '',
|
|
100
|
+
attachments_json TEXT NOT NULL DEFAULT '[]',
|
|
101
|
+
reply_to_message_id TEXT NOT NULL DEFAULT '',
|
|
102
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
103
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
104
|
+
created_at INTEGER NOT NULL,
|
|
105
|
+
updated_at INTEGER NOT NULL,
|
|
106
|
+
UNIQUE(bot_name, chat_id, delivery_key),
|
|
107
|
+
UNIQUE(session_key, source_type, source_id)
|
|
108
|
+
);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_delivery_outbox_pending ON delivery_outbox(status, created_at);
|
|
110
|
+
`);
|
|
111
|
+
// Migration: add delivery_outbox delivery key columns if missing
|
|
112
|
+
try {
|
|
113
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN delivery_key TEXT NOT NULL DEFAULT ''`);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Column already exists
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN content_hash TEXT NOT NULL DEFAULT ''`);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Column already exists
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN reply_to_message_id TEXT NOT NULL DEFAULT ''`);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Column already exists
|
|
129
|
+
}
|
|
130
|
+
// Backfill stable keys for rows created by earlier outbox experiments before
|
|
131
|
+
// creating indexes that reference the new columns.
|
|
132
|
+
this.db.exec(`
|
|
133
|
+
UPDATE delivery_outbox
|
|
134
|
+
SET delivery_key = source_id
|
|
135
|
+
WHERE delivery_key IS NULL OR delivery_key = '';
|
|
85
136
|
`);
|
|
137
|
+
try {
|
|
138
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_delivery_outbox_key ON delivery_outbox(bot_name, chat_id, delivery_key)`);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Existing duplicate experimental rows can prevent index creation in dev DB.
|
|
142
|
+
// Fresh DBs still get the table-level UNIQUE constraint; dirty DBs still use
|
|
143
|
+
// source unique + short-window content hash until cleaned.
|
|
144
|
+
}
|
|
145
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_delivery_outbox_content ON delivery_outbox(bot_name, chat_id, content_hash, created_at)`);
|
|
146
|
+
// Migration: add delivery_outbox reply target if missing
|
|
147
|
+
try {
|
|
148
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN reply_to_message_id TEXT NOT NULL DEFAULT ''`);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Column already exists
|
|
152
|
+
}
|
|
86
153
|
// Migration: add verbose column if missing
|
|
87
154
|
try {
|
|
88
155
|
this.db.exec(`ALTER TABLE chat_info ADD COLUMN verbose INTEGER NOT NULL DEFAULT 0`);
|
|
@@ -197,6 +264,104 @@ export class MessageStore {
|
|
|
197
264
|
VALUES (?, ?, ?, ?, ?)
|
|
198
265
|
`).run(botName, chatId, triggerMessageRowId, Date.now(), replyMessageId);
|
|
199
266
|
}
|
|
267
|
+
enqueueDelivery(item) {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const result = this.db.prepare(`
|
|
270
|
+
INSERT OR IGNORE INTO delivery_outbox
|
|
271
|
+
(session_key, chat_id, bot_name, source_type, source_id, delivery_key, content_hash, content, attachments_json, reply_to_message_id, status, attempts, created_at, updated_at)
|
|
272
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)
|
|
273
|
+
`).run(item.sessionKey, item.chatId, item.botName, item.sourceType, item.sourceId, item.deliveryKey, item.contentHash, item.content, item.attachmentsJson, item.replyToMessageId || '', now, now);
|
|
274
|
+
return result.changes ? Number(result.lastInsertRowid) : null;
|
|
275
|
+
}
|
|
276
|
+
getDeliveryBySource(sessionKey, sourceType, sourceId) {
|
|
277
|
+
const row = this.db.prepare(`
|
|
278
|
+
SELECT * FROM delivery_outbox
|
|
279
|
+
WHERE session_key = ? AND source_type = ? AND source_id = ?
|
|
280
|
+
`).get(sessionKey, sourceType, sourceId);
|
|
281
|
+
return row ? this.mapDelivery(row) : null;
|
|
282
|
+
}
|
|
283
|
+
getPendingDeliveries(chatId, botName, maxCount = 20) {
|
|
284
|
+
if (chatId && botName) {
|
|
285
|
+
const rows = this.db.prepare(`
|
|
286
|
+
SELECT * FROM delivery_outbox
|
|
287
|
+
WHERE status = 'pending' AND chat_id = ? AND bot_name = ?
|
|
288
|
+
ORDER BY created_at ASC LIMIT ?
|
|
289
|
+
`).all(chatId, botName, maxCount);
|
|
290
|
+
return rows.map((r) => this.mapDelivery(r));
|
|
291
|
+
}
|
|
292
|
+
const rows = this.db.prepare(`
|
|
293
|
+
SELECT * FROM delivery_outbox WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?
|
|
294
|
+
`).all(maxCount);
|
|
295
|
+
return rows.map((r) => this.mapDelivery(r));
|
|
296
|
+
}
|
|
297
|
+
hasRecentSimilarDelivery(botName, chatId, contentHash, windowMs) {
|
|
298
|
+
if (!contentHash)
|
|
299
|
+
return false;
|
|
300
|
+
const row = this.db.prepare(`
|
|
301
|
+
SELECT 1 FROM delivery_outbox
|
|
302
|
+
WHERE bot_name = ? AND chat_id = ? AND content_hash = ? AND created_at >= ? AND status IN ('pending', 'delivering', 'delivered')
|
|
303
|
+
LIMIT 1
|
|
304
|
+
`).get(botName, chatId, contentHash, Date.now() - windowMs);
|
|
305
|
+
return !!row;
|
|
306
|
+
}
|
|
307
|
+
hasRecentOverlappingDelivery(botName, chatId, content, attachmentsJson, windowMs, minShortLength = 8) {
|
|
308
|
+
const normalized = content.trim();
|
|
309
|
+
if (normalized.length < minShortLength)
|
|
310
|
+
return false;
|
|
311
|
+
const rows = this.db.prepare(`
|
|
312
|
+
SELECT content, attachments_json FROM delivery_outbox
|
|
313
|
+
WHERE bot_name = ? AND chat_id = ? AND created_at >= ? AND status IN ('pending', 'delivering', 'delivered')
|
|
314
|
+
ORDER BY created_at DESC
|
|
315
|
+
LIMIT 20
|
|
316
|
+
`).all(botName, chatId, Date.now() - windowMs);
|
|
317
|
+
for (const row of rows) {
|
|
318
|
+
if ((row.attachments_json || '[]') !== attachmentsJson)
|
|
319
|
+
continue;
|
|
320
|
+
const existing = String(row.content || '').trim();
|
|
321
|
+
if (existing.length < minShortLength)
|
|
322
|
+
continue;
|
|
323
|
+
const shorter = existing.length <= normalized.length ? existing : normalized;
|
|
324
|
+
const longer = existing.length <= normalized.length ? normalized : existing;
|
|
325
|
+
if (shorter.length >= minShortLength && longer.includes(shorter))
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
claimDelivery(id) {
|
|
331
|
+
const result = this.db.prepare(`
|
|
332
|
+
UPDATE delivery_outbox SET status = 'delivering', attempts = attempts + 1, updated_at = ? WHERE id = ? AND status = 'pending'
|
|
333
|
+
`).run(Date.now(), id);
|
|
334
|
+
return result.changes === 1;
|
|
335
|
+
}
|
|
336
|
+
markDeliveryDelivered(id) {
|
|
337
|
+
this.db.prepare(`
|
|
338
|
+
UPDATE delivery_outbox SET status = 'delivered', updated_at = ? WHERE id = ?
|
|
339
|
+
`).run(Date.now(), id);
|
|
340
|
+
}
|
|
341
|
+
markDeliveryFailed(id) {
|
|
342
|
+
this.db.prepare(`
|
|
343
|
+
UPDATE delivery_outbox SET status = 'failed', updated_at = ? WHERE id = ?
|
|
344
|
+
`).run(Date.now(), id);
|
|
345
|
+
}
|
|
346
|
+
mapDelivery(row) {
|
|
347
|
+
return {
|
|
348
|
+
id: row.id,
|
|
349
|
+
sessionKey: row.session_key,
|
|
350
|
+
chatId: row.chat_id,
|
|
351
|
+
botName: row.bot_name,
|
|
352
|
+
sourceType: row.source_type,
|
|
353
|
+
sourceId: row.source_id,
|
|
354
|
+
deliveryKey: row.delivery_key || row.source_id,
|
|
355
|
+
contentHash: row.content_hash || '',
|
|
356
|
+
content: row.content || '',
|
|
357
|
+
attachmentsJson: row.attachments_json || '[]',
|
|
358
|
+
replyToMessageId: row.reply_to_message_id || '',
|
|
359
|
+
status: row.status,
|
|
360
|
+
attempts: row.attempts || 0,
|
|
361
|
+
createdAt: row.created_at,
|
|
362
|
+
updatedAt: row.updated_at,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
200
365
|
/**
|
|
201
366
|
* Get messages that haven't been synced to a bot's session yet.
|
|
202
367
|
* Returns messages ordered by timestamp ascending.
|
|
@@ -25,12 +25,16 @@ export declare class OpenClawClient {
|
|
|
25
25
|
private sessionMessageCallbacks;
|
|
26
26
|
/** Session keys that should be re-subscribed on reconnect */
|
|
27
27
|
private subscribedKeys;
|
|
28
|
-
/** Session keys with active/recent chatSend —
|
|
28
|
+
/** Session keys with active/recent chatSend — proactive is forwarded and deduped by outbox. */
|
|
29
29
|
private suppressedSessions;
|
|
30
30
|
private suppressedSessionTimers;
|
|
31
|
+
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
32
|
+
private mutedProactiveSessions;
|
|
33
|
+
private mutedProactiveSessionCounts;
|
|
31
34
|
constructor(config: OpenClawConfig);
|
|
32
35
|
connect(): Promise<void>;
|
|
33
36
|
private _doConnect;
|
|
37
|
+
private handleProactiveSessionMessage;
|
|
34
38
|
private scheduleReconnect;
|
|
35
39
|
private rpc;
|
|
36
40
|
/**
|
|
@@ -71,6 +75,9 @@ export declare class OpenClawClient {
|
|
|
71
75
|
abortChat(sessionKey: string, runId: string): Promise<any>;
|
|
72
76
|
private suppressSessionKeys;
|
|
73
77
|
private releaseSuppressedSessionKeysAfter;
|
|
78
|
+
private addMutedProactiveKey;
|
|
79
|
+
private releaseMutedProactiveKey;
|
|
80
|
+
muteProactiveDelivery(sessionKey: string): (delayMs?: number) => void;
|
|
74
81
|
chatSend(params: {
|
|
75
82
|
sessionKey: string;
|
|
76
83
|
message: string;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -23,9 +23,12 @@ export class OpenClawClient {
|
|
|
23
23
|
sessionMessageCallbacks = new Map();
|
|
24
24
|
/** Session keys that should be re-subscribed on reconnect */
|
|
25
25
|
subscribedKeys = new Set();
|
|
26
|
-
/** Session keys with active/recent chatSend —
|
|
26
|
+
/** Session keys with active/recent chatSend — proactive is forwarded and deduped by outbox. */
|
|
27
27
|
suppressedSessions = new Set();
|
|
28
28
|
suppressedSessionTimers = new Map();
|
|
29
|
+
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
30
|
+
mutedProactiveSessions = new Set();
|
|
31
|
+
mutedProactiveSessionCounts = new Map();
|
|
29
32
|
constructor(config) {
|
|
30
33
|
this.config = config;
|
|
31
34
|
}
|
|
@@ -140,47 +143,7 @@ export class OpenClawClient {
|
|
|
140
143
|
// Try both raw key and without agent:main: prefix
|
|
141
144
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
142
145
|
const msg = frame.payload.message || frame.payload;
|
|
143
|
-
|
|
144
|
-
const content = msg.content;
|
|
145
|
-
// Proactive assistant text messages (suppress during active chatSend).
|
|
146
|
-
// Cron/session-targeted runs often emit structured content arrays rather
|
|
147
|
-
// than a plain string. Extract only visible text parts and ignore thinking
|
|
148
|
-
// / tool blocks so the bridge can deliver final cron results via the bot.
|
|
149
|
-
let proactiveText = "";
|
|
150
|
-
if (role === "assistant") {
|
|
151
|
-
if (typeof content === "string") {
|
|
152
|
-
proactiveText = content;
|
|
153
|
-
}
|
|
154
|
-
else if (Array.isArray(content)) {
|
|
155
|
-
const hasToolBlock = content.some((part) => {
|
|
156
|
-
const type = String(part?.type || "").toLowerCase();
|
|
157
|
-
return type === "toolcall" || type === "tool_call" || type === "tooluse" || type === "tool_use" || type === "toolresult" || type === "tool_result";
|
|
158
|
-
});
|
|
159
|
-
// Do not deliver mixed text+toolCall assistant messages through
|
|
160
|
-
// the proactive final-text path; those are usually intermediate
|
|
161
|
-
// reasoning/status during a tool loop. Tool calls are still
|
|
162
|
-
// delivered via the verbose channel from agent item events when
|
|
163
|
-
// /verbose is enabled. Cron final messages arrive as text-only
|
|
164
|
-
// (optionally with thinking).
|
|
165
|
-
if (!hasToolBlock) {
|
|
166
|
-
proactiveText = content
|
|
167
|
-
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
168
|
-
.map((part) => part.text)
|
|
169
|
-
.join("\n")
|
|
170
|
-
.trim();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (proactiveText) {
|
|
175
|
-
if (this.suppressedSessions.has(rawKey) || this.suppressedSessions.has(shortKey)) {
|
|
176
|
-
console.log(`[OpenClaw] Suppressing proactive msg for ${shortKey} (active chatSend)`);
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
180
|
-
if (cb)
|
|
181
|
-
cb(proactiveText);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
146
|
+
this.handleProactiveSessionMessage(rawKey, msg);
|
|
184
147
|
// Tool calls in assistant messages — skip, using agent item events instead
|
|
185
148
|
// (session.message toolCall events are batched, not real-time)
|
|
186
149
|
}
|
|
@@ -212,6 +175,53 @@ export class OpenClawClient {
|
|
|
212
175
|
});
|
|
213
176
|
});
|
|
214
177
|
}
|
|
178
|
+
handleProactiveSessionMessage(rawKey, msg) {
|
|
179
|
+
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
180
|
+
const role = msg.role;
|
|
181
|
+
const content = msg.content;
|
|
182
|
+
// Proactive assistant text messages. Cron/session-targeted runs often emit
|
|
183
|
+
// structured content arrays rather than a plain string. Extract only visible
|
|
184
|
+
// text parts and ignore thinking/tool blocks so the bridge can deliver final
|
|
185
|
+
// cron results via the bot.
|
|
186
|
+
let proactiveText = "";
|
|
187
|
+
if (role === "assistant") {
|
|
188
|
+
if (typeof content === "string") {
|
|
189
|
+
proactiveText = content;
|
|
190
|
+
}
|
|
191
|
+
else if (Array.isArray(content)) {
|
|
192
|
+
const hasToolBlock = content.some((part) => {
|
|
193
|
+
const type = String(part?.type || "").toLowerCase();
|
|
194
|
+
return type === "toolcall" || type === "tool_call" || type === "tooluse" || type === "tool_use" || type === "toolresult" || type === "tool_result";
|
|
195
|
+
});
|
|
196
|
+
// Do not deliver mixed text+toolCall assistant messages through
|
|
197
|
+
// the proactive final-text path; those are usually intermediate
|
|
198
|
+
// reasoning/status during a tool loop. Tool calls are still
|
|
199
|
+
// delivered via the verbose channel from agent item events when
|
|
200
|
+
// /verbose is enabled. Cron final messages arrive as text-only
|
|
201
|
+
// (optionally with thinking).
|
|
202
|
+
if (!hasToolBlock) {
|
|
203
|
+
proactiveText = content
|
|
204
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
205
|
+
.map((part) => part.text)
|
|
206
|
+
.join("\n")
|
|
207
|
+
.trim();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!proactiveText)
|
|
212
|
+
return false;
|
|
213
|
+
if (this.mutedProactiveSessions.has(rawKey) || this.mutedProactiveSessions.has(shortKey)) {
|
|
214
|
+
console.log(`[OpenClaw] Dropping proactive msg for ${shortKey}; delivery is owned by the caller`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (this.suppressedSessions.has(rawKey) || this.suppressedSessions.has(shortKey)) {
|
|
218
|
+
console.log(`[OpenClaw] Forwarding proactive msg for ${shortKey} during active chatSend; delivery outbox will dedupe`);
|
|
219
|
+
}
|
|
220
|
+
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
221
|
+
if (cb)
|
|
222
|
+
cb(proactiveText);
|
|
223
|
+
return Boolean(cb);
|
|
224
|
+
}
|
|
215
225
|
scheduleReconnect() {
|
|
216
226
|
const delay = this.reconnectDelay;
|
|
217
227
|
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
@@ -494,6 +504,42 @@ export class OpenClawClient {
|
|
|
494
504
|
this.suppressedSessionTimers.set(key, timer);
|
|
495
505
|
}
|
|
496
506
|
}
|
|
507
|
+
addMutedProactiveKey(key) {
|
|
508
|
+
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
509
|
+
this.mutedProactiveSessionCounts.set(key, count + 1);
|
|
510
|
+
this.mutedProactiveSessions.add(key);
|
|
511
|
+
}
|
|
512
|
+
releaseMutedProactiveKey(key) {
|
|
513
|
+
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
514
|
+
if (count <= 1) {
|
|
515
|
+
this.mutedProactiveSessionCounts.delete(key);
|
|
516
|
+
this.mutedProactiveSessions.delete(key);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
this.mutedProactiveSessionCounts.set(key, count - 1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
muteProactiveDelivery(sessionKey) {
|
|
523
|
+
const shortKey = sessionKey.startsWith("agent:main:") ? sessionKey.slice("agent:main:".length) : sessionKey;
|
|
524
|
+
const fullKey = `agent:main:${shortKey}`;
|
|
525
|
+
const keys = [shortKey, fullKey];
|
|
526
|
+
for (const key of keys)
|
|
527
|
+
this.addMutedProactiveKey(key);
|
|
528
|
+
let released = false;
|
|
529
|
+
return (delayMs = 0) => {
|
|
530
|
+
if (released)
|
|
531
|
+
return;
|
|
532
|
+
released = true;
|
|
533
|
+
const release = () => {
|
|
534
|
+
for (const key of keys)
|
|
535
|
+
this.releaseMutedProactiveKey(key);
|
|
536
|
+
};
|
|
537
|
+
if (delayMs > 0)
|
|
538
|
+
setTimeout(release, delayMs);
|
|
539
|
+
else
|
|
540
|
+
release();
|
|
541
|
+
};
|
|
542
|
+
}
|
|
497
543
|
async chatSend(params) {
|
|
498
544
|
const sk = params.sessionKey;
|
|
499
545
|
const fullSessionKey = `agent:main:${sk}`;
|
|
@@ -621,6 +667,8 @@ export class OpenClawClient {
|
|
|
621
667
|
clearTimeout(timer);
|
|
622
668
|
this.suppressedSessionTimers.clear();
|
|
623
669
|
this.suppressedSessions.clear();
|
|
670
|
+
this.mutedProactiveSessions.clear();
|
|
671
|
+
this.mutedProactiveSessionCounts.clear();
|
|
624
672
|
if (this.ws) {
|
|
625
673
|
this.ws.close();
|
|
626
674
|
this.ws = null;
|