openclaw-lark-multi-agent 0.1.11 → 0.1.13

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.
@@ -25,6 +25,7 @@ export type DiscussionParticipant = {
25
25
  export declare class DiscussionManager {
26
26
  private sessions;
27
27
  private seenRoots;
28
+ private readonly seenRootTtlMs;
28
29
  isActive(chatId: string): boolean;
29
30
  stop(chatId: string): boolean;
30
31
  status(chatId: string): DiscussionSession | null;
@@ -36,6 +37,7 @@ export declare class DiscussionManager {
36
37
  participants: DiscussionParticipant[];
37
38
  sendSystemMessage?: (text: string) => Promise<void>;
38
39
  }): boolean;
40
+ private pruneSeenRoots;
39
41
  private runLoop;
40
42
  private buildPrompt;
41
43
  }
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "crypto";
2
2
  export class DiscussionManager {
3
3
  sessions = new Map();
4
- seenRoots = new Set();
4
+ seenRoots = new Map();
5
+ seenRootTtlMs = 6 * 60 * 60 * 1000;
5
6
  isActive(chatId) {
6
7
  return this.sessions.get(chatId)?.status === "running";
7
8
  }
@@ -18,15 +19,18 @@ export class DiscussionManager {
18
19
  return session ? { ...session, completedRounds: [...session.completedRounds] } : null;
19
20
  }
20
21
  startIfAbsent(params) {
22
+ this.pruneSeenRoots();
21
23
  const key = `${params.chatId}:${params.rootMessageId}`;
22
24
  if (this.seenRoots.has(key))
23
25
  return false;
24
- this.seenRoots.add(key);
26
+ this.seenRoots.set(key, Date.now());
25
27
  if (this.isActive(params.chatId))
26
28
  this.stop(params.chatId);
27
29
  const participants = params.participants.filter((p, index, arr) => arr.findIndex((x) => x.name === p.name) === index);
28
- if (participants.length === 0)
30
+ if (participants.length === 0) {
31
+ this.seenRoots.delete(key);
29
32
  return false;
33
+ }
30
34
  const session = {
31
35
  id: randomUUID(),
32
36
  chatId: params.chatId,
@@ -47,6 +51,12 @@ export class DiscussionManager {
47
51
  });
48
52
  return true;
49
53
  }
54
+ pruneSeenRoots(now = Date.now()) {
55
+ for (const [key, ts] of this.seenRoots) {
56
+ if (now - ts > this.seenRootTtlMs)
57
+ this.seenRoots.delete(key);
58
+ }
59
+ }
50
60
  async runLoop(sessionId, participants, sendSystemMessage) {
51
61
  while (true) {
52
62
  const session = Array.from(this.sessions.values()).find((s) => s.id === sessionId);
@@ -457,7 +457,7 @@ export class FeishuBot {
457
457
  const next = current === "free" ? "normal" : "free";
458
458
  this.store.setBotMode(this.config.name, chatId, next);
459
459
  if (next === "free") {
460
- await this.replyMessage(messageId, `🔓 ${this.config.name} 已切换到 free 模式\n不需要 @ 也可以参与回复(连续 Bot 回复超过 ${MAX_BOT_STREAK} 轮将暂停,等待人类发言)`);
460
+ await this.replyMessage(messageId, `🔓 ${this.config.name} 已切换到 free 模式\n不需要 @ 也可以回复普通人类消息;如果消息明确 @ 了其他 bot 或普通人,我不会抢答。\n如需多轮自动讨论,请使用群级命令 /discuss on。`);
461
461
  }
462
462
  else {
463
463
  await this.replyMessage(messageId, `🔒 ${this.config.name} 已切换到 normal 模式\n只有明确 @ 我才会回复`);
@@ -635,7 +635,7 @@ export class FeishuBot {
635
635
  deliver: false,
636
636
  // Keep bridge UX responsive; long agent/tool loops should surface a clear failure
637
637
  // instead of leaving reactions stuck forever.
638
- timeoutMs: 600000,
638
+ timeoutMs: 1_800_000,
639
639
  });
640
640
  console.log(`[${this.config.name}] OpenClaw reply collected for ${chatId.slice(-8)} in ${Date.now() - queueStartedAt}ms`);
641
641
  const parsedReply = this.extractBridgeAttachments(reply);
@@ -921,7 +921,7 @@ export class FeishuBot {
921
921
  currentMessage: prompt,
922
922
  currentSenderName: "Discussion Scheduler",
923
923
  deliver: false,
924
- timeoutMs: 600000,
924
+ timeoutMs: 1_800_000,
925
925
  });
926
926
  const parsedReply = this.extractBridgeAttachments(reply);
927
927
  const visibleReply = parsedReply.text.trim();
@@ -266,22 +266,28 @@ export class OpenClawClient {
266
266
  let replayInvalidTimer = null;
267
267
  const collectStartedAt = Date.now();
268
268
  let lifecycleStartedLogged = false;
269
- const timer = setTimeout(() => {
270
- clearInterval(poller);
271
- if (chatFinalTimer)
272
- clearTimeout(chatFinalTimer);
273
- if (lifecycleEndTimer)
274
- clearTimeout(lifecycleEndTimer);
275
- if (replayInvalidTimer)
276
- clearTimeout(replayInvalidTimer);
277
- console.warn(`[OpenClaw] collectReply timeout for runId=${runId} sessionKey=${sessionKey}`);
278
- this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
279
- console.warn(`[OpenClaw] abort after collectReply timeout failed:`, err.message);
280
- });
281
- resolve(text || chatFinalText || "(timeout: no reply received)");
282
- }, timeoutMs);
269
+ let idleTimer;
270
+ const resetIdleTimer = () => {
271
+ if (idleTimer)
272
+ clearTimeout(idleTimer);
273
+ idleTimer = setTimeout(() => {
274
+ clearInterval(poller);
275
+ if (chatFinalTimer)
276
+ clearTimeout(chatFinalTimer);
277
+ if (lifecycleEndTimer)
278
+ clearTimeout(lifecycleEndTimer);
279
+ if (replayInvalidTimer)
280
+ clearTimeout(replayInvalidTimer);
281
+ console.warn(`[OpenClaw] collectReply idle timeout for runId=${runId} sessionKey=${sessionKey}`);
282
+ this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
283
+ console.warn(`[OpenClaw] abort after collectReply idle timeout failed:`, err.message);
284
+ });
285
+ resolve(text || chatFinalText || "(timeout: no reply received)");
286
+ }, timeoutMs);
287
+ };
288
+ resetIdleTimer();
283
289
  const finish = (finalText) => {
284
- clearTimeout(timer);
290
+ clearTimeout(idleTimer);
285
291
  clearInterval(poller);
286
292
  if (chatFinalTimer)
287
293
  clearTimeout(chatFinalTimer);
@@ -318,6 +324,10 @@ export class OpenClawClient {
318
324
  continue;
319
325
  }
320
326
  bucket.splice(i, 1);
327
+ // Any matching event — including toolCall/toolResult/item/lifecycle —
328
+ // means the agent is still alive. Use an idle timeout, not an absolute
329
+ // wall-clock timeout, so long tool-heavy tasks are not killed while active.
330
+ resetIdleTimer();
321
331
  // If more events arrive after a replay-invalid lifecycle end, that lifecycle
322
332
  // was not terminal for the user-visible run. Keep waiting for the real final.
323
333
  if (replayInvalidTimer) {
@@ -342,7 +352,14 @@ export class OpenClawClient {
342
352
  });
343
353
  // Prefer final chat message over accumulated deltas: some providers may
344
354
  // emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
345
- finish(chatFinalText || text);
355
+ const latestFinalText = chatFinalText || text;
356
+ if (latestFinalText) {
357
+ finish(latestFinalText);
358
+ }
359
+ else {
360
+ console.warn(`[OpenClaw] collectReply: empty chatFinal fallback ignored; waiting for real text or idle timeout`);
361
+ chatFinalTimer = null;
362
+ }
346
363
  }, 5000);
347
364
  }
348
365
  }
@@ -359,7 +376,7 @@ export class OpenClawClient {
359
376
  finish("NO_REPLY");
360
377
  return;
361
378
  }
362
- if (!latestFinalText && ev.data?.livenessState !== "working") {
379
+ if (!latestFinalText) {
363
380
  const state = ev.data?.livenessState || "unknown";
364
381
  const reason = ev.data?.stopReason || "";
365
382
  const replayInvalid = ev.data?.replayInvalid ? ", replayInvalid" : "";
@@ -369,11 +386,14 @@ export class OpenClawClient {
369
386
  replayInvalidTimer = setTimeout(() => finish(failureText), 120000);
370
387
  return;
371
388
  }
372
- finish(failureText);
373
- }
374
- else {
375
- finish(latestFinalText);
389
+ if (ev.data?.livenessState !== "working") {
390
+ finish(failureText);
391
+ return;
392
+ }
393
+ console.warn(`[OpenClaw] empty lifecycle end ignored for runId=${evRunId || runId}; waiting for real text or idle timeout`);
394
+ return;
376
395
  }
396
+ finish(latestFinalText);
377
397
  };
378
398
  // If lifecycle end beats chat final, a short delta like "N" can be a truncated
379
399
  // final reply. Wait for chatFinal before resolving; otherwise suppress lone "N".
@@ -386,7 +406,7 @@ export class OpenClawClient {
386
406
  return;
387
407
  }
388
408
  if (ev.stream === "lifecycle" && ev.data?.phase === "error") {
389
- clearTimeout(timer);
409
+ clearTimeout(idleTimer);
390
410
  clearInterval(poller);
391
411
  if (chatFinalTimer)
392
412
  clearTimeout(chatFinalTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
5
5
  "type": "module",
6
6
  "scripts": {