openclaw-lark-multi-agent 1.0.3 → 1.0.5
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.js +3 -2
- package/dist/feishu-bot.d.ts +1 -0
- package/dist/feishu-bot.js +20 -1
- package/dist/index.js +22 -1
- package/dist/openclaw-client.d.ts +15 -1
- package/dist/openclaw-client.js +140 -14
- package/package.json +1 -1
|
@@ -133,10 +133,11 @@ export class DiscussionManager {
|
|
|
133
133
|
buildPrompt(session) {
|
|
134
134
|
const previous = session.completedRounds.length === 0
|
|
135
135
|
? "(暂无,当前是第一轮)"
|
|
136
|
-
:
|
|
136
|
+
: (() => {
|
|
137
|
+
const round = session.completedRounds[session.completedRounds.length - 1];
|
|
137
138
|
const lines = Object.entries(round.replies).map(([bot, text]) => `- ${bot}: ${text || "NO_REPLY"}`);
|
|
138
139
|
return `Round ${round.round}:\n${lines.join("\n")}`;
|
|
139
|
-
})
|
|
140
|
+
})();
|
|
140
141
|
return [
|
|
141
142
|
"这是一个多智能体结构化讨论。",
|
|
142
143
|
"",
|
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -146,6 +146,7 @@ export declare class FeishuBot {
|
|
|
146
146
|
/**
|
|
147
147
|
* Send a model-drift notification to the affected chat.
|
|
148
148
|
*/
|
|
149
|
+
private hydrateInlineImageKeys;
|
|
149
150
|
/**
|
|
150
151
|
* Download a resource (image/file/audio) from a Feishu message.
|
|
151
152
|
* Returns the local file path.
|
package/dist/feishu-bot.js
CHANGED
|
@@ -360,7 +360,7 @@ export class FeishuBot {
|
|
|
360
360
|
}
|
|
361
361
|
else if (messageType === "post") {
|
|
362
362
|
// Rich text post - extract all text content
|
|
363
|
-
cleanText = this.extractPostText(content);
|
|
363
|
+
cleanText = await this.hydrateInlineImageKeys(this.extractPostText(content), messageId);
|
|
364
364
|
}
|
|
365
365
|
else if (messageType === "sticker") {
|
|
366
366
|
cleanText = `[Sticker: ${content.file_key || "unknown"}]`;
|
|
@@ -1107,6 +1107,7 @@ export class FeishuBot {
|
|
|
1107
1107
|
currentSenderName: "Discussion Scheduler",
|
|
1108
1108
|
deliver: false,
|
|
1109
1109
|
timeoutMs: 1_800_000,
|
|
1110
|
+
emptyFinalAsNoReply: true,
|
|
1110
1111
|
});
|
|
1111
1112
|
}
|
|
1112
1113
|
finally {
|
|
@@ -1588,6 +1589,24 @@ export class FeishuBot {
|
|
|
1588
1589
|
/**
|
|
1589
1590
|
* Send a model-drift notification to the affected chat.
|
|
1590
1591
|
*/
|
|
1592
|
+
async hydrateInlineImageKeys(text, messageId) {
|
|
1593
|
+
const imageKeyPattern = /\[Image: (img_[^\]\n]+)\]/g;
|
|
1594
|
+
const replacements = [];
|
|
1595
|
+
for (const match of text.matchAll(imageKeyPattern)) {
|
|
1596
|
+
const imageKey = match[1];
|
|
1597
|
+
try {
|
|
1598
|
+
const imgPath = await this.downloadResource(messageId, imageKey, "image");
|
|
1599
|
+
replacements.push({ from: match[0], to: `[Image: ${imgPath}]` });
|
|
1600
|
+
}
|
|
1601
|
+
catch (err) {
|
|
1602
|
+
replacements.push({ from: match[0], to: `[Image: download failed - ${err.message}]` });
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
let out = text;
|
|
1606
|
+
for (const r of replacements)
|
|
1607
|
+
out = out.replace(r.from, r.to);
|
|
1608
|
+
return out;
|
|
1609
|
+
}
|
|
1591
1610
|
/**
|
|
1592
1611
|
* Download a resource (image/file/audio) from a Feishu message.
|
|
1593
1612
|
* Returns the local file path.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { loadConfig } from "./config.js";
|
|
|
2
2
|
import { OpenClawClient } from "./openclaw-client.js";
|
|
3
3
|
import { MessageStore } from "./message-store.js";
|
|
4
4
|
import { FeishuBot } from "./feishu-bot.js";
|
|
5
|
-
import { mkdirSync } from "fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
6
|
import { dirname, resolve } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
export async function startApp(configPath) {
|
|
@@ -19,6 +19,22 @@ export async function startApp(configPath) {
|
|
|
19
19
|
: resolve(dirname(resolvedConfigPath), "data");
|
|
20
20
|
mkdirSync(dataDir, { recursive: true });
|
|
21
21
|
console.log(`Data dir: ${dataDir}`);
|
|
22
|
+
const lockPath = resolve(dataDir, "lma.pid");
|
|
23
|
+
if (existsSync(lockPath)) {
|
|
24
|
+
const oldPid = Number(readFileSync(lockPath, "utf8").trim());
|
|
25
|
+
if (Number.isFinite(oldPid) && oldPid > 0) {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(oldPid, 0);
|
|
28
|
+
throw new Error(`Another openclaw-lark-multi-agent instance is already running (pid ${oldPid}). Stop it before starting a new one.`);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const code = err.code;
|
|
32
|
+
if (code !== "ESRCH")
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
writeFileSync(lockPath, `${process.pid}\n`);
|
|
22
38
|
const store = new MessageStore(resolve(dataDir, "messages.db"));
|
|
23
39
|
// Connect to OpenClaw Gateway via WebSocket
|
|
24
40
|
const openclawClient = new OpenClawClient(config.openclaw);
|
|
@@ -39,6 +55,11 @@ export async function startApp(configPath) {
|
|
|
39
55
|
console.log("\nShutting down...");
|
|
40
56
|
openclawClient.disconnect();
|
|
41
57
|
store.close();
|
|
58
|
+
try {
|
|
59
|
+
if (readFileSync(lockPath, "utf8").trim() === String(process.pid))
|
|
60
|
+
rmSync(lockPath, { force: true });
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
42
63
|
process.exit(0);
|
|
43
64
|
};
|
|
44
65
|
process.on("SIGINT", shutdown);
|
|
@@ -6,6 +6,8 @@ export type ChatAttachment = {
|
|
|
6
6
|
fileName?: string;
|
|
7
7
|
content: string;
|
|
8
8
|
};
|
|
9
|
+
export declare const GATEWAY_PROTOCOL_MIN = 3;
|
|
10
|
+
export declare const GATEWAY_PROTOCOL_MAX = 4;
|
|
9
11
|
/**
|
|
10
12
|
* OpenClaw Gateway WebSocket client.
|
|
11
13
|
* Full agent pipeline — tools, memory, skills, context management by OpenClaw.
|
|
@@ -25,9 +27,12 @@ export declare class OpenClawClient {
|
|
|
25
27
|
private sessionMessageCallbacks;
|
|
26
28
|
/** Session keys that should be re-subscribed on reconnect */
|
|
27
29
|
private subscribedKeys;
|
|
28
|
-
/** Session keys
|
|
30
|
+
/** Session keys whose transcript/session.message updates are currently suppressed. */
|
|
29
31
|
private suppressedSessions;
|
|
30
32
|
private suppressedSessionTimers;
|
|
33
|
+
/** Session keys whose delivery is owned by this bridge's chatSend final path. */
|
|
34
|
+
private ownedDeliverySessions;
|
|
35
|
+
private ownedDeliverySessionTimers;
|
|
31
36
|
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
32
37
|
private mutedProactiveSessions;
|
|
33
38
|
private mutedProactiveSessionCounts;
|
|
@@ -73,8 +78,15 @@ export declare class OpenClawClient {
|
|
|
73
78
|
* deliver=false prevents OpenClaw from auto-posting to channels.
|
|
74
79
|
*/
|
|
75
80
|
abortChat(sessionKey: string, runId: string): Promise<any>;
|
|
81
|
+
private sessionKeyVariants;
|
|
82
|
+
private trackChatEventSession;
|
|
83
|
+
private extractTextFromChatMessage;
|
|
84
|
+
private emitProactiveForSession;
|
|
76
85
|
private suppressSessionKeys;
|
|
77
86
|
private releaseSuppressedSessionKeysAfter;
|
|
87
|
+
private ownDeliverySessionKeys;
|
|
88
|
+
private releaseOwnedDeliverySessionKeysAfter;
|
|
89
|
+
private isOwnedDeliverySession;
|
|
78
90
|
private addMutedProactiveKey;
|
|
79
91
|
private releaseMutedProactiveKey;
|
|
80
92
|
muteProactiveDelivery(sessionKey: string): (delayMs?: number) => void;
|
|
@@ -84,6 +96,7 @@ export declare class OpenClawClient {
|
|
|
84
96
|
attachments?: ChatAttachment[];
|
|
85
97
|
deliver?: boolean;
|
|
86
98
|
timeoutMs?: number;
|
|
99
|
+
emptyFinalAsNoReply?: boolean;
|
|
87
100
|
}): Promise<string>;
|
|
88
101
|
private shouldInjectBridgeAttachmentHint;
|
|
89
102
|
private bridgeAttachmentHint;
|
|
@@ -107,6 +120,7 @@ export declare class OpenClawClient {
|
|
|
107
120
|
currentSenderName: string;
|
|
108
121
|
deliver?: boolean;
|
|
109
122
|
timeoutMs?: number;
|
|
123
|
+
emptyFinalAsNoReply?: boolean;
|
|
110
124
|
}): Promise<string>;
|
|
111
125
|
private extractImageAttachments;
|
|
112
126
|
disconnect(): Promise<void>;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -3,6 +3,8 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { basename, extname } from "path";
|
|
5
5
|
import { getBridgeAttachmentsDir } from "./paths.js";
|
|
6
|
+
export const GATEWAY_PROTOCOL_MIN = 3;
|
|
7
|
+
export const GATEWAY_PROTOCOL_MAX = 4;
|
|
6
8
|
const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
|
|
7
9
|
/**
|
|
8
10
|
* OpenClaw Gateway WebSocket client.
|
|
@@ -23,9 +25,12 @@ export class OpenClawClient {
|
|
|
23
25
|
sessionMessageCallbacks = new Map();
|
|
24
26
|
/** Session keys that should be re-subscribed on reconnect */
|
|
25
27
|
subscribedKeys = new Set();
|
|
26
|
-
/** Session keys
|
|
28
|
+
/** Session keys whose transcript/session.message updates are currently suppressed. */
|
|
27
29
|
suppressedSessions = new Set();
|
|
28
30
|
suppressedSessionTimers = new Map();
|
|
31
|
+
/** Session keys whose delivery is owned by this bridge's chatSend final path. */
|
|
32
|
+
ownedDeliverySessions = new Set();
|
|
33
|
+
ownedDeliverySessionTimers = new Map();
|
|
29
34
|
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
30
35
|
mutedProactiveSessions = new Set();
|
|
31
36
|
mutedProactiveSessionCounts = new Map();
|
|
@@ -60,8 +65,8 @@ export class OpenClawClient {
|
|
|
60
65
|
id: "connect-1",
|
|
61
66
|
method: "connect",
|
|
62
67
|
params: {
|
|
63
|
-
minProtocol:
|
|
64
|
-
maxProtocol:
|
|
68
|
+
minProtocol: GATEWAY_PROTOCOL_MIN,
|
|
69
|
+
maxProtocol: GATEWAY_PROTOCOL_MAX,
|
|
65
70
|
client: {
|
|
66
71
|
id: "gateway-client",
|
|
67
72
|
version: "1.0.0",
|
|
@@ -111,8 +116,24 @@ export class OpenClawClient {
|
|
|
111
116
|
// Normalize chat events to look like agent events for collectReply
|
|
112
117
|
if (frame.event === "chat") {
|
|
113
118
|
const state = frame.payload?.state;
|
|
119
|
+
this.trackChatEventSession(sk, state, frame.payload);
|
|
114
120
|
const msg = frame.payload?.message;
|
|
115
|
-
if (state === "
|
|
121
|
+
if (state === "delta") {
|
|
122
|
+
// v4: chat delta events carry deltaText (incremental text chunk)
|
|
123
|
+
const deltaText = frame.payload?.deltaText;
|
|
124
|
+
if (deltaText) {
|
|
125
|
+
this.agentEvents.get(sk).push({
|
|
126
|
+
...frame.payload,
|
|
127
|
+
stream: "chatDelta",
|
|
128
|
+
data: {
|
|
129
|
+
deltaText,
|
|
130
|
+
delta: deltaText, // v3 compat
|
|
131
|
+
replace: frame.payload?.replace || false,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (state === "final") {
|
|
116
137
|
// Store chat final text as fallback (only used if agent stream had no text)
|
|
117
138
|
const textParts = [];
|
|
118
139
|
if (msg?.content) {
|
|
@@ -215,7 +236,8 @@ export class OpenClawClient {
|
|
|
215
236
|
return false;
|
|
216
237
|
}
|
|
217
238
|
if (this.suppressedSessions.has(rawKey) || this.suppressedSessions.has(shortKey)) {
|
|
218
|
-
console.log(`[OpenClaw]
|
|
239
|
+
console.log(`[OpenClaw] Dropping proactive transcript msg for ${shortKey}; waiting for final delivery path`);
|
|
240
|
+
return false;
|
|
219
241
|
}
|
|
220
242
|
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
221
243
|
if (cb)
|
|
@@ -266,9 +288,10 @@ export class OpenClawClient {
|
|
|
266
288
|
* No aggressive timeout — waits for lifecycle end as the source of truth.
|
|
267
289
|
* 30-minute safety net only for catastrophic WS disconnection.
|
|
268
290
|
*/
|
|
269
|
-
collectReply(runId, timeoutMs = 1800000, targetSessionKey) {
|
|
291
|
+
collectReply(runId, timeoutMs = 1800000, targetSessionKey, options) {
|
|
270
292
|
return new Promise((resolve, reject) => {
|
|
271
293
|
let text = "";
|
|
294
|
+
let chatDeltaText = "";
|
|
272
295
|
let chatFinalText = "";
|
|
273
296
|
let sessionKey = targetSessionKey ? `agent:main:${targetSessionKey}` : "";
|
|
274
297
|
let chatFinalTimer = null;
|
|
@@ -292,7 +315,7 @@ export class OpenClawClient {
|
|
|
292
315
|
this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
|
|
293
316
|
console.warn(`[OpenClaw] abort after collectReply idle timeout failed:`, err.message);
|
|
294
317
|
});
|
|
295
|
-
resolve(text || chatFinalText || "(timeout: no reply received)");
|
|
318
|
+
resolve(text || chatDeltaText || chatFinalText || "(timeout: no reply received)");
|
|
296
319
|
}, timeoutMs);
|
|
297
320
|
};
|
|
298
321
|
resetIdleTimer();
|
|
@@ -348,8 +371,24 @@ export class OpenClawClient {
|
|
|
348
371
|
lifecycleStartedLogged = true;
|
|
349
372
|
console.log(`[OpenClaw] lifecycle start for runId=${runId} after ${Date.now() - collectStartedAt}ms`);
|
|
350
373
|
}
|
|
351
|
-
if (ev.stream === "assistant" && ev.data?.delta) {
|
|
352
|
-
|
|
374
|
+
if ((ev.stream === "assistant" || ev.stream === "chatDelta") && (ev.data?.deltaText || ev.data?.delta)) {
|
|
375
|
+
const chunk = ev.data.deltaText || ev.data.delta;
|
|
376
|
+
if (ev.stream === "assistant") {
|
|
377
|
+
if (ev.data?.replace) {
|
|
378
|
+
text = chunk;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
text += chunk;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
if (ev.data?.replace) {
|
|
386
|
+
chatDeltaText = chunk;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
chatDeltaText += chunk;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
353
392
|
}
|
|
354
393
|
if (ev.stream === "chatFinal") {
|
|
355
394
|
chatFinalText = ev.data?.text || "";
|
|
@@ -362,10 +401,13 @@ export class OpenClawClient {
|
|
|
362
401
|
});
|
|
363
402
|
// Prefer final chat message over accumulated deltas: some providers may
|
|
364
403
|
// emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
|
|
365
|
-
const latestFinalText = chatFinalText || text;
|
|
404
|
+
const latestFinalText = chatFinalText || text || chatDeltaText;
|
|
366
405
|
if (latestFinalText) {
|
|
367
406
|
finish(latestFinalText);
|
|
368
407
|
}
|
|
408
|
+
else if (options?.emptyFinalAsNoReply) {
|
|
409
|
+
finish("NO_REPLY");
|
|
410
|
+
}
|
|
369
411
|
else {
|
|
370
412
|
console.warn(`[OpenClaw] collectReply: empty chatFinal fallback ignored; waiting for real text or idle timeout`);
|
|
371
413
|
chatFinalTimer = null;
|
|
@@ -376,9 +418,9 @@ export class OpenClawClient {
|
|
|
376
418
|
if (ev.stream === "lifecycle" && ev.data?.phase === "end") {
|
|
377
419
|
// Prefer final chat message over accumulated deltas: some providers may
|
|
378
420
|
// emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
|
|
379
|
-
const finalText = chatFinalText || text;
|
|
421
|
+
const finalText = chatFinalText || text || chatDeltaText;
|
|
380
422
|
const finishFromLifecycle = () => {
|
|
381
|
-
const latestFinalText = chatFinalText || text;
|
|
423
|
+
const latestFinalText = chatFinalText || text || chatDeltaText;
|
|
382
424
|
if (!chatFinalText && latestFinalText.trim() === "N") {
|
|
383
425
|
// Some providers stream the first character of NO_REPLY ("N") but
|
|
384
426
|
// never deliver a final chat message in time. Never surface a lone
|
|
@@ -386,6 +428,10 @@ export class OpenClawClient {
|
|
|
386
428
|
finish("NO_REPLY");
|
|
387
429
|
return;
|
|
388
430
|
}
|
|
431
|
+
if (!latestFinalText && options?.emptyFinalAsNoReply) {
|
|
432
|
+
finish("NO_REPLY");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
389
435
|
if (!latestFinalText) {
|
|
390
436
|
const state = ev.data?.livenessState || "unknown";
|
|
391
437
|
const reason = ev.data?.stopReason || "";
|
|
@@ -407,7 +453,7 @@ export class OpenClawClient {
|
|
|
407
453
|
};
|
|
408
454
|
// If lifecycle end beats chat final, a short delta like "N" can be a truncated
|
|
409
455
|
// final reply. Wait for chatFinal before resolving; otherwise suppress lone "N".
|
|
410
|
-
if (!chatFinalText && text.length <= 1) {
|
|
456
|
+
if (!options?.emptyFinalAsNoReply && !chatFinalText && text.length <= 1) {
|
|
411
457
|
lifecycleEndTimer = setTimeout(finishFromLifecycle, 5000);
|
|
412
458
|
}
|
|
413
459
|
else {
|
|
@@ -483,6 +529,53 @@ export class OpenClawClient {
|
|
|
483
529
|
const key = sessionKey.startsWith("agent:main:") ? sessionKey.slice("agent:main:".length) : sessionKey;
|
|
484
530
|
return this.rpc("chat.abort", { sessionKey: key, runId }, 5000).catch(() => { });
|
|
485
531
|
}
|
|
532
|
+
sessionKeyVariants(key) {
|
|
533
|
+
const shortKey = key.startsWith("agent:main:") ? key.slice("agent:main:".length) : key;
|
|
534
|
+
return [shortKey, `agent:main:${shortKey}`];
|
|
535
|
+
}
|
|
536
|
+
trackChatEventSession(sessionKey, state, payload) {
|
|
537
|
+
if (!sessionKey || sessionKey === "__default__")
|
|
538
|
+
return;
|
|
539
|
+
const keys = this.sessionKeyVariants(sessionKey);
|
|
540
|
+
if (this.isOwnedDeliverySession(sessionKey)) {
|
|
541
|
+
// This chat event belongs to a bridge-owned chat.send run. The final answer
|
|
542
|
+
// is delivered by collectReply/processQueue, so transcript session.message
|
|
543
|
+
// mirrors must stay suppressed briefly.
|
|
544
|
+
this.suppressSessionKeys(keys);
|
|
545
|
+
if (state === "final" || state === "error" || state === "aborted")
|
|
546
|
+
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// External WebChat/Control UI chat against an LMA session: do not forward
|
|
550
|
+
// streaming transcript updates, but allow the final chat message to be
|
|
551
|
+
// delivered through the proactive callback after it is committed.
|
|
552
|
+
if (state === "delta") {
|
|
553
|
+
this.suppressSessionKeys(keys);
|
|
554
|
+
}
|
|
555
|
+
else if (state === "final") {
|
|
556
|
+
const text = this.extractTextFromChatMessage(payload?.message);
|
|
557
|
+
if (text)
|
|
558
|
+
this.emitProactiveForSession(sessionKey, text);
|
|
559
|
+
// Keep transcript mirrors suppressed briefly; chat final already emitted
|
|
560
|
+
// the user-visible result for external WebChat/Control UI turns.
|
|
561
|
+
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
562
|
+
}
|
|
563
|
+
else if (state === "error" || state === "aborted") {
|
|
564
|
+
this.releaseSuppressedSessionKeysAfter(keys, 0);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
extractTextFromChatMessage(message) {
|
|
568
|
+
const parts = Array.isArray(message?.content) ? message.content : [];
|
|
569
|
+
return parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
|
|
570
|
+
}
|
|
571
|
+
emitProactiveForSession(sessionKey, text) {
|
|
572
|
+
const [shortKey, fullKey] = this.sessionKeyVariants(sessionKey);
|
|
573
|
+
const cb = this.sessionMessageCallbacks.get(fullKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
574
|
+
if (!cb)
|
|
575
|
+
return false;
|
|
576
|
+
cb(text);
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
486
579
|
suppressSessionKeys(keys) {
|
|
487
580
|
for (const key of keys) {
|
|
488
581
|
const timer = this.suppressedSessionTimers.get(key);
|
|
@@ -504,6 +597,31 @@ export class OpenClawClient {
|
|
|
504
597
|
this.suppressedSessionTimers.set(key, timer);
|
|
505
598
|
}
|
|
506
599
|
}
|
|
600
|
+
ownDeliverySessionKeys(keys) {
|
|
601
|
+
for (const key of keys) {
|
|
602
|
+
const timer = this.ownedDeliverySessionTimers.get(key);
|
|
603
|
+
if (timer)
|
|
604
|
+
clearTimeout(timer);
|
|
605
|
+
this.ownedDeliverySessionTimers.delete(key);
|
|
606
|
+
this.ownedDeliverySessions.add(key);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
releaseOwnedDeliverySessionKeysAfter(keys, delayMs) {
|
|
610
|
+
for (const key of keys) {
|
|
611
|
+
const oldTimer = this.ownedDeliverySessionTimers.get(key);
|
|
612
|
+
if (oldTimer)
|
|
613
|
+
clearTimeout(oldTimer);
|
|
614
|
+
const timer = setTimeout(() => {
|
|
615
|
+
this.ownedDeliverySessions.delete(key);
|
|
616
|
+
this.ownedDeliverySessionTimers.delete(key);
|
|
617
|
+
}, delayMs);
|
|
618
|
+
this.ownedDeliverySessionTimers.set(key, timer);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
isOwnedDeliverySession(sessionKey) {
|
|
622
|
+
const [shortKey, fullKey] = this.sessionKeyVariants(sessionKey);
|
|
623
|
+
return this.ownedDeliverySessions.has(shortKey) || this.ownedDeliverySessions.has(fullKey);
|
|
624
|
+
}
|
|
507
625
|
addMutedProactiveKey(key) {
|
|
508
626
|
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
509
627
|
this.mutedProactiveSessionCounts.set(key, count + 1);
|
|
@@ -545,6 +663,7 @@ export class OpenClawClient {
|
|
|
545
663
|
const fullSessionKey = `agent:main:${sk}`;
|
|
546
664
|
const suppressedKeys = [sk, fullSessionKey];
|
|
547
665
|
this.suppressSessionKeys(suppressedKeys);
|
|
666
|
+
this.ownDeliverySessionKeys(suppressedKeys);
|
|
548
667
|
try {
|
|
549
668
|
// Drop stale buffered events for this session before starting a new run.
|
|
550
669
|
// This prevents an old final text (e.g. previous "ok") from being consumed by
|
|
@@ -560,7 +679,7 @@ export class OpenClawClient {
|
|
|
560
679
|
idempotencyKey: randomUUID(),
|
|
561
680
|
});
|
|
562
681
|
console.log(`[OpenClaw] chat.send runId: ${result.runId} (rpc=${Date.now() - sendStartedAt}ms, attachments=${params.attachments?.length || 0})`);
|
|
563
|
-
return await this.collectReply(result.runId, params.timeoutMs || 1800000, sk);
|
|
682
|
+
return await this.collectReply(result.runId, params.timeoutMs || 1800000, sk, { emptyFinalAsNoReply: params.emptyFinalAsNoReply });
|
|
564
683
|
}
|
|
565
684
|
finally {
|
|
566
685
|
// OpenClaw can emit the final assistant session.message a moment after
|
|
@@ -568,6 +687,7 @@ export class OpenClawClient {
|
|
|
568
687
|
// are not delivered twice via the proactive-message path. Cron/LMA runs
|
|
569
688
|
// are unaffected because they do not go through chatSend.
|
|
570
689
|
this.releaseSuppressedSessionKeysAfter(suppressedKeys, 30000);
|
|
690
|
+
this.releaseOwnedDeliverySessionKeysAfter(suppressedKeys, 30000);
|
|
571
691
|
}
|
|
572
692
|
}
|
|
573
693
|
shouldInjectBridgeAttachmentHint(text) {
|
|
@@ -610,6 +730,7 @@ export class OpenClawClient {
|
|
|
610
730
|
attachments,
|
|
611
731
|
deliver: params.deliver,
|
|
612
732
|
timeoutMs: params.timeoutMs,
|
|
733
|
+
emptyFinalAsNoReply: params.emptyFinalAsNoReply,
|
|
613
734
|
});
|
|
614
735
|
}
|
|
615
736
|
// Build context block + actual message in one chat.send
|
|
@@ -632,6 +753,7 @@ export class OpenClawClient {
|
|
|
632
753
|
attachments,
|
|
633
754
|
deliver: params.deliver,
|
|
634
755
|
timeoutMs: params.timeoutMs,
|
|
756
|
+
emptyFinalAsNoReply: params.emptyFinalAsNoReply,
|
|
635
757
|
});
|
|
636
758
|
}
|
|
637
759
|
extractImageAttachments(contents) {
|
|
@@ -669,6 +791,10 @@ export class OpenClawClient {
|
|
|
669
791
|
this.suppressedSessions.clear();
|
|
670
792
|
this.mutedProactiveSessions.clear();
|
|
671
793
|
this.mutedProactiveSessionCounts.clear();
|
|
794
|
+
for (const timer of this.ownedDeliverySessionTimers.values())
|
|
795
|
+
clearTimeout(timer);
|
|
796
|
+
this.ownedDeliverySessionTimers.clear();
|
|
797
|
+
this.ownedDeliverySessions.clear();
|
|
672
798
|
if (this.ws) {
|
|
673
799
|
this.ws.close();
|
|
674
800
|
this.ws = null;
|