openclaw-lark-multi-agent 1.8.8 → 1.8.9

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.
@@ -1458,14 +1458,15 @@ export class FeishuBot {
1458
1458
  // 息都发个这个"). Stay silent until it persists through several
1459
1459
  // re-checks (~45s), then notify exactly once.
1460
1460
  const unknownChecks = state === 'unknown' ? (Number(item.unknownChecks) || 0) + 1 : 0;
1461
- const unknownNotified = Boolean(item.unknownNotified) || (state === 'unknown' && unknownChecks >= 3);
1462
- const record = { ...item, attempted: valid ? (receipt.attempted ?? true) : true, sessionId: item.sessionId, reason: receipt.reason, unknownChecks, unknownNotified };
1461
+ const record = { ...item, attempted: valid ? (receipt.attempted ?? true) : true, sessionId: item.sessionId, reason: receipt.reason, unknownChecks };
1463
1462
  this.store.saveInsertion(this.config.name, chatId, item.rowId, item.insertionId, state, record, Date.now() + (state === 'unknown' ? 15_000 : 5000));
1464
1463
  if (state === 'ordinary') {
1464
+ this.store.deletePendingInsertionNotices(this.config.name, chatId, item.insertionId);
1465
1465
  void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:ordinary`, 'ℹ️ 原 run 已明确结束,且这条消息确定未被插入;现已交回普通队列,只执行一次。', [], item.messageId, `insertion:${item.insertionId}:ordinary`).catch(() => { });
1466
1466
  void this.processQueue(chatId).catch(() => { });
1467
1467
  }
1468
1468
  else if (state === 'accepted') {
1469
+ this.store.deletePendingInsertionNotices(this.config.name, chatId, item.insertionId);
1469
1470
  this.store.clearPendingTrigger(this.config.name, chatId, item.rowId);
1470
1471
  // Durable acceptance drives Get, including after bridge restart when
1471
1472
  // its ephemeral pendingAckMessages array no longer exists.
@@ -1486,8 +1487,17 @@ export class FeishuBot {
1486
1487
  if (item.state !== state)
1487
1488
  void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:${state}`, `ℹ️ 这条插入消息的状态为 ${state},仍保留本地记录,未确认接收前不会显示 Get,也不会自动重复执行。${receipt.reason || ''}`, [], item.messageId, `insertion:${item.insertionId}:${state}`).catch(() => { });
1488
1489
  }
1489
- else if (state === 'unknown' && unknownNotified && !item.unknownNotified) {
1490
- void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:unknown-prolonged`, `ℹ️ 这条插入消息约 45 秒仍未确认接收:本地记录保留中,不会自动重复执行;确认后会自动显示 Get。`, [], item.messageId, `insertion:${item.insertionId}:unknown-prolonged`).catch(() => { });
1490
+ else if (state === 'unknown' && unknownChecks >= 4 && !item.unknownNotified) {
1491
+ // ~45s (attempt + 3 re-checks). Mark unknownNotified ONLY after the
1492
+ // outbox insert succeeds, so a failed enqueue retries next check
1493
+ // instead of silencing the notice forever.
1494
+ void this.enqueueAndDispatchDelivery(chatId, 'insertion_status', `insertion:${item.insertionId}:unknown-prolonged`, `ℹ️ 这条插入消息约 45 秒仍未确认接收:本地记录保留中,不会自动重复执行;确认后会自动显示 Get。`, [], item.messageId, `insertion:${item.insertionId}:unknown-prolonged`)
1495
+ .then(() => {
1496
+ const cur = this.store.getInsertion(this.config.name, chatId, item.rowId);
1497
+ const curRecord = (cur && cur.record) || record;
1498
+ this.store.saveInsertion(this.config.name, chatId, item.rowId, item.insertionId, (cur && cur.state) || state, { ...curRecord, unknownNotified: true }, Date.now() + 15_000);
1499
+ })
1500
+ .catch(() => { });
1491
1501
  }
1492
1502
  }
1493
1503
  }
@@ -131,6 +131,9 @@ export declare class MessageStore {
131
131
  finishRunObservation(sessionKey: string, runId: string, state?: string): void;
132
132
  listRunObservations(sessionKey?: string, due?: number, limit?: number): any[];
133
133
  saveInsertion(bot: string, chat: string, row: number, id: string, state: string, record: object, next?: number): void;
134
+ /** Purge still-pending insertion status notices once the insertion resolves
135
+ * (accepted/ordinary): a stale "unknown" notice must not fire after Get. */
136
+ deletePendingInsertionNotices(bot: string, chat: string, insertionId: string): number;
134
137
  claimInsertionAttempt(bot: string, chat: string, row: number): boolean;
135
138
  getInsertion(bot: string, chat: string, row: number): any;
136
139
  listInsertions(bot: string, chat?: string): any[];
@@ -159,6 +159,13 @@ export class MessageStore {
159
159
  ON CONFLICT(bot_name,chat_id,message_row_id) DO UPDATE SET state=excluded.state,record_json=excluded.record_json,next_check_at=excluded.next_check_at
160
160
  WHERE insertion_custody.state NOT IN ('stopped','committed')`).run(bot, chat, row, id, JSON.stringify(record), state, next);
161
161
  }
162
+ /** Purge still-pending insertion status notices once the insertion resolves
163
+ * (accepted/ordinary): a stale "unknown" notice must not fire after Get. */
164
+ deletePendingInsertionNotices(bot, chat, insertionId) {
165
+ const r = this.db.prepare(`DELETE FROM delivery_outbox WHERE bot_name=? AND chat_id=? AND delivery_key LIKE ? AND status='pending'`)
166
+ .run(bot, chat, `insertion:${insertionId}:%`);
167
+ return r.changes;
168
+ }
162
169
  claimInsertionAttempt(bot, chat, row) {
163
170
  return this.db.prepare(`UPDATE insertion_custody SET state='dispatching',record_json=json_set(record_json,'$.attempted',json('true'))
164
171
  WHERE bot_name=? AND chat_id=? AND message_row_id=? AND state='pending'`).run(bot, chat, row).changes === 1;
@@ -900,11 +900,12 @@ export class OpenClawClient {
900
900
  let lifecycleEndTimer = null;
901
901
  let replayInvalidTimer = null;
902
902
  let genericChatErrorTimer = null;
903
- // A channel message-tool send marked terminate=true is the turn's intended
904
- // user-visible output (2026-10-16 incident: it bypassed the bridge and the
905
- // collector waited forever). Capture its text and settle on a short grace.
906
- let terminatingSendText = "";
907
- let terminatingSendTimer = null;
903
+ // A terminating message-tool send schedules ONE deferred exact-run
904
+ // reconciliation. The timer carries no settlement or delivery authority
905
+ // of its own; checkRunState still requires the gateway to confirm the
906
+ // root's recorded terminal state (all tools settled, no error, no async
907
+ // pending) before anything resolves.
908
+ let terminatingReconcileTimer = null;
908
909
  let pendingRuntimeFailureText = "";
909
910
  let lastActivitySummary = "";
910
911
  let lastActivityAt = 0;
@@ -1179,8 +1180,8 @@ export class OpenClawClient {
1179
1180
  clearTimeout(replayInvalidTimer);
1180
1181
  if (genericChatErrorTimer)
1181
1182
  clearTimeout(genericChatErrorTimer);
1182
- if (terminatingSendTimer)
1183
- clearTimeout(terminatingSendTimer);
1183
+ if (terminatingReconcileTimer)
1184
+ clearTimeout(terminatingReconcileTimer);
1184
1185
  resolve(finalText);
1185
1186
  };
1186
1187
  const collectEvents = () => {
@@ -1317,33 +1318,21 @@ export class OpenClawClient {
1317
1318
  if (hasEffectiveActivity(ev))
1318
1319
  resetIdleTimer();
1319
1320
  rememberActivity(ev);
1320
- // Terminating channel-send detection: a message-tool result carrying
1321
- // terminate=true ends the turn with that send as its user-visible
1322
- // output. The bridge is the delivery channel for LMA sessions, so
1323
- // waiting for a further final would hang forever (2026-10-16 frozen
1324
- // card incident). Capture the sent text at tool start; settle on a
1325
- // short grace unless a real final arrives first.
1321
+ // Terminating channel-send detection (2026-10-16 frozen-card
1322
+ // incident, remediated per independent review): a message-tool
1323
+ // result carrying a top-level terminate=true is a batch-control
1324
+ // hint — NOT a delivery receipt and NOT settlement authority for
1325
+ // this collector. It only nudges the exact-run reconciliation:
1326
+ // checkRunState requires a recorded terminal time, all tools
1327
+ // settled, no error and no async pending before resolving; a run
1328
+ // that is still executing keeps waiting normally. No tool input is
1329
+ // ever re-delivered to the source chat by this path.
1326
1330
  {
1327
1331
  const tdata = ev.data || {};
1328
1332
  if (ev.stream === "tool" || (ev.stream === "item" && tdata.kind === "tool")) {
1329
1333
  const toolName = String(tdata.name || tdata.toolName || "");
1330
1334
  const tphase = tdata.phase === "result" ? (tdata.isError || tdata.error ? "error" : "end") : tdata.phase;
1331
- if (toolName === "message" && tphase === "start") {
1332
- const raw = tdata.input ?? tdata.args ?? tdata.meta ?? null;
1333
- if (raw !== null) {
1334
- try {
1335
- const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1336
- if (parsed && typeof parsed.message === "string" && parsed.message.trim())
1337
- terminatingSendText = parsed.message;
1338
- }
1339
- catch { /* non-JSON tool input */ }
1340
- }
1341
- }
1342
- else if (toolName === "message" && (tphase === "end" || tphase === "error")) {
1343
- // Top-level check ONLY: a read/reaction result whose content
1344
- // merely mentions "terminate=true" (e.g. quoting this very
1345
- // bug report in chat) must never be mistaken for a
1346
- // terminating send.
1335
+ if (toolName === "message" && (tphase === "end" || tphase === "error")) {
1347
1336
  const rawOut = tdata.output ?? tdata.result ?? tdata.error ?? null;
1348
1337
  let terminating = false;
1349
1338
  try {
@@ -1360,15 +1349,14 @@ export class OpenClawClient {
1360
1349
  }
1361
1350
  }
1362
1351
  catch { /* unparseable output is not evidence */ }
1363
- if (terminating && !terminatingSendTimer) {
1364
- terminatingSendTimer = setTimeout(() => {
1365
- terminatingSendTimer = null;
1352
+ if (terminating && !terminatingReconcileTimer) {
1353
+ terminatingReconcileTimer = setTimeout(() => {
1354
+ terminatingReconcileTimer = null;
1366
1355
  if (observationClosed)
1367
1356
  return;
1368
- const best = (chatFinalText.trim() || text.trim() || terminatingSendText || "").trim();
1369
- finish(best || "NO_REPLY");
1357
+ void checkRunState(() => true, true);
1370
1358
  }, 10_000);
1371
- terminatingSendTimer.unref?.();
1359
+ terminatingReconcileTimer.unref?.();
1372
1360
  }
1373
1361
  }
1374
1362
  }
@@ -1632,6 +1620,8 @@ export class OpenClawClient {
1632
1620
  clearTimeout(lifecycleEndTimer);
1633
1621
  if (genericChatErrorTimer)
1634
1622
  clearTimeout(genericChatErrorTimer);
1623
+ if (terminatingReconcileTimer)
1624
+ clearTimeout(terminatingReconcileTimer);
1635
1625
  // Abort the (possibly still-running) backgrounded run before we
1636
1626
  // reject, so an auto-retry probe does not race a zombie run on the
1637
1627
  // same session. Idempotent/best-effort.
@@ -1688,8 +1678,8 @@ export class OpenClawClient {
1688
1678
  clearTimeout(replayInvalidTimer);
1689
1679
  if (genericChatErrorTimer)
1690
1680
  clearTimeout(genericChatErrorTimer);
1691
- if (terminatingSendTimer)
1692
- clearTimeout(terminatingSendTimer);
1681
+ if (terminatingReconcileTimer)
1682
+ clearTimeout(terminatingReconcileTimer);
1693
1683
  if (observationKey && this.runStateChecks.get(observationKey) === checkAfterNoActiveRun)
1694
1684
  this.runStateChecks.delete(observationKey);
1695
1685
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.8.8",
3
+ "version": "1.8.9",
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": {