openclaw-lark-multi-agent 1.8.12 → 1.8.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.
@@ -354,8 +354,16 @@ export declare class FeishuBot {
354
354
  /**
355
355
  * Enqueue a message send to guarantee ordering per chat.
356
356
  * All sends for a chat are serialized through this.
357
+ *
358
+ * The chain must never wedge: a single hung Feishu call used to block every
359
+ * later send in that chat forever (live-status patches fire per tool event),
360
+ * which froze cards and stranded /stop confirmations. Each link is therefore
361
+ * raced against a deadline, so a stuck send is abandoned and ordering resumes.
362
+ * Prefer a larger OPENCLAW_LARK_MULTI_AGENT_SEND_TIMEOUT_MS on slow links.
357
363
  */
358
364
  private sendOrdered;
365
+ /** One send link: resolve with fn(), or reject at the deadline so the chain advances. */
366
+ private withSendDeadline;
359
367
  /**
360
368
  * Add a reaction (emoji) to a message.
361
369
  */
@@ -29,6 +29,9 @@ const SESSION_HEALTH_POLL_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_SESS
29
29
  const SESSION_HEALTH_CONFIRM_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_SESSION_HEALTH_CONFIRM_MS || 2_000);
30
30
  const DELIVERY_MAX_ATTEMPTS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_DELIVERY_MAX_ATTEMPTS || 5);
31
31
  const DELIVERY_RETRY_BASE_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_DELIVERY_RETRY_BASE_MS || 1_000);
32
+ // Per-chat send serialization deadline. A hung Feishu call must not block the
33
+ // chain forever, otherwise live-status patches and /stop confirmations strand.
34
+ const SEND_DEADLINE_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_SEND_TIMEOUT_MS || 30_000);
32
35
  // Auto-retry: when a reply looks truncated/incomplete, ask the same session
33
36
  // whether it finished. If it did not, let it continue; loop until it confirms
34
37
  // completion or the retry budget is spent. Off by setting AUTO_RETRY=0.
@@ -1361,10 +1364,20 @@ export class FeishuBot {
1361
1364
  return;
1362
1365
  }
1363
1366
  const identified = compatible && !!activity.activeRunId && !!activity.sessionId && !activity.archived && !status.archived;
1367
+ // Bounded retention (2026-10-17 deadlock): an input that has stayed
1368
+ // activity-unbound for over 60s despite re-checks must not be held forever.
1369
+ // Hand it to the ordinary queue — the gateway's own chat.send admission is
1370
+ // the authoritative answer and admission custody already dedups.
1371
+ const unboundSince = current?.activityUnbound ? (current.unboundSince || 0) : 0;
1372
+ if (!identified && unboundSince && Date.now() - unboundSince > 60_000) {
1373
+ this.store.saveInsertion(this.config.name, chatId, rowId, current.insertionId, 'ordinary', { ...current, activityUnbound: false, reason: 'gateway state unconfirmable over 60s; ordinary admission is authoritative' });
1374
+ return;
1375
+ }
1364
1376
  this.store.saveInsertion(this.config.name, chatId, rowId, current?.insertionId || JSON.stringify([this.config.appId, chatId, messageId]), 'pending', {
1365
1377
  messageId, text, stopEpoch: epoch, sessionKey: this.getSessionKey(chatId), sessionId: identified ? activity.sessionId : status.sessionId,
1366
1378
  expectedRunId: identified ? activity.activeRunId : '', ownerId: identified ? `external:${activity.activeRunId}` : undefined,
1367
1379
  batchId: identified ? activity.activeRunId : undefined, activityUnbound: !identified, attempted: false,
1380
+ ...(identified ? {} : { unboundSince: unboundSince || Date.now() }),
1368
1381
  reason: identified ? 'current Gateway run observed; insertion custody only' : 'current Gateway activity/run identity unavailable; retained without admission',
1369
1382
  }, Date.now() + (identified ? 0 : 5000));
1370
1383
  }
@@ -4628,13 +4641,46 @@ export class FeishuBot {
4628
4641
  /**
4629
4642
  * Enqueue a message send to guarantee ordering per chat.
4630
4643
  * All sends for a chat are serialized through this.
4644
+ *
4645
+ * The chain must never wedge: a single hung Feishu call used to block every
4646
+ * later send in that chat forever (live-status patches fire per tool event),
4647
+ * which froze cards and stranded /stop confirmations. Each link is therefore
4648
+ * raced against a deadline, so a stuck send is abandoned and ordering resumes.
4649
+ * Prefer a larger OPENCLAW_LARK_MULTI_AGENT_SEND_TIMEOUT_MS on slow links.
4631
4650
  */
4632
4651
  sendOrdered(chatId, fn) {
4633
4652
  const prev = this.sendQueue.get(chatId) || Promise.resolve();
4634
- const next = prev.then(fn, fn); // run even if previous failed
4653
+ const run = () => this.withSendDeadline(fn, chatId);
4654
+ const next = prev.then(run, run); // run even if previous failed
4635
4655
  this.sendQueue.set(chatId, next.then(() => undefined, () => undefined));
4636
4656
  return next;
4637
4657
  }
4658
+ /** One send link: resolve with fn(), or reject at the deadline so the chain advances. */
4659
+ async withSendDeadline(fn, chatId) {
4660
+ const limit = SEND_DEADLINE_MS;
4661
+ let work;
4662
+ try {
4663
+ work = Promise.resolve(fn());
4664
+ }
4665
+ catch (err) {
4666
+ return Promise.reject(err);
4667
+ }
4668
+ // A late rejection from an abandoned send must never become an unhandled
4669
+ // rejection; the caller already moved on with the timeout error.
4670
+ work.catch(() => { });
4671
+ let timer;
4672
+ const deadline = new Promise((_, reject) => {
4673
+ timer = setTimeout(() => reject(new Error(`send deadline ${limit}ms exceeded for ${chatId.slice(-8)}`)), limit);
4674
+ timer.unref?.();
4675
+ });
4676
+ try {
4677
+ return await Promise.race([work, deadline]);
4678
+ }
4679
+ finally {
4680
+ if (timer)
4681
+ clearTimeout(timer);
4682
+ }
4683
+ }
4638
4684
  /**
4639
4685
  * Add a reaction (emoji) to a message.
4640
4686
  */
@@ -2177,8 +2177,8 @@ export class OpenClawClient {
2177
2177
  let timer;
2178
2178
  try {
2179
2179
  const value = await Promise.race([
2180
- this.rpc("sessions.describe", { key: this.canonicalSessionKey(sessionKey) }, 1500),
2181
- new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("status lookup timeout")), 1500); }),
2180
+ this.rpc("sessions.describe", { key: this.canonicalSessionKey(sessionKey) }, 5000),
2181
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("status lookup timeout")), 5000); }),
2182
2182
  ]);
2183
2183
  return normalizeSessionRuntimeStatus(value);
2184
2184
  }
@@ -2194,7 +2194,7 @@ export class OpenClawClient {
2194
2194
  * terminal caches. Unknown/missing fields never authorize scheduling. */
2195
2195
  async getSessionDispatchStatus(sessionKey) {
2196
2196
  let timer;
2197
- const key = this.canonicalSessionKey(sessionKey), deadline = Date.now() + 1500;
2197
+ const key = this.canonicalSessionKey(sessionKey), deadline = Date.now() + 8000;
2198
2198
  // One budget covers the entire public pending scan, not 1500ms per page.
2199
2199
  // Retained interrupted/cancelled inputs are recovery records, NOT runnable
2200
2200
  // queue entries. Never delete, mutate or replay them to prove schedulability.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.8.12",
3
+ "version": "1.8.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": {
@@ -83,4 +83,4 @@
83
83
  "publishConfig": {
84
84
  "access": "public"
85
85
  }
86
- }
86
+ }