openclaw-lark-multi-agent 1.2.4 → 1.2.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.
@@ -122,14 +122,6 @@ export declare class FeishuBot {
122
122
  private isBridgeControlReply;
123
123
  private isLegacyBridgeControlMessage;
124
124
  private isRuntimeFailureText;
125
- /**
126
- * Is this main-run error worth auto-retrying (by resuming the session via the
127
- * probe loop)? Mainly transient failures — request/response timeouts and
128
- * aborted/incomplete runs. We do NOT auto-retry clearly terminal errors
129
- * (auth/permission/quota/billing/model-not-supported), which would just fail
130
- * again. The error text comes from collectReply's `Agent error: <msg>`.
131
- */
132
- private isRetryableAgentError;
133
125
  /**
134
126
  * Heuristic: does this reply look truncated / mid-task? Bias toward false
135
127
  * positives (“宁可错杀不放过”) — a false positive only costs one invisible
@@ -182,6 +174,9 @@ export declare class FeishuBot {
182
174
  * session kept working and its new output becomes the latest reply, which we
183
175
  * re-check. Loops until the session confirms completion, a reply no longer
184
176
  * looks truncated, the budget is spent, or the session cannot answer.
177
+ *
178
+ * This is the ONLY auto-retry trigger: a reply that LOOKS UNFINISHED. We do not
179
+ * retry on transient run errors, and we do not auto-compact-and-retry.
185
180
  */
186
181
  private autoRetryUntilComplete;
187
182
  private cancelDelayedFailure;
@@ -29,10 +29,6 @@ function autoRetryMax() { return Number(process.env.OPENCLAW_LARK_MULTI_AGENT_AU
29
29
  // for the locale-appropriate phrase; detection accepts BOTH so either works.
30
30
  const AUTO_RETRY_DONE_PHRASE = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_DONE_PHRASE || "\u7ed3\u675f\u4e86";
31
31
  const AUTO_RETRY_DONE_PHRASE_EN = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_DONE_PHRASE_EN || "DONE";
32
- // When an auto-retry probe errors AND context usage is at/above this percent,
33
- // auto-compact the session once and keep retrying (instead of giving up). This
34
- // rescues the “session too heavy → run times out / lock contention” failure mode.
35
- function autoRetryCompactPct() { return Number(process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_COMPACT_PCT || 50); }
36
32
  // How many of the most-recent tool calls (and their results) tool-trim keeps
37
33
  // intact — recent tool calls may still be relevant to the agent's next step.
38
34
  function toolTrimKeepRecent() { return Number(process.env.OPENCLAW_LARK_MULTI_AGENT_TOOLTRIM_KEEP_RECENT || 3); }
@@ -1248,29 +1244,9 @@ export class FeishuBot {
1248
1244
  }));
1249
1245
  }
1250
1246
  catch (mainErr) {
1251
- // The main run failed before returning a reply. If it is a retryable
1252
- // error (e.g. a request timeout) and not a session-health failure,
1253
- // reuse the SAME auto-retry mechanism to let the session resume from
1254
- // where it was interrupted, instead of giving up immediately.
1255
- if (mainErr instanceof UnhealthySessionError)
1256
- throw mainErr;
1257
- const errText = mainErr?.message || String(mainErr);
1258
- if (autoRetryEnabled() && !isNativeCommandTrigger && this.isRetryableAgentError(errText)) {
1259
- console.warn(`[${this.config.name}] main run failed (${errText.slice(0, 80)}) for ${chatId.slice(-8)}; entering auto-retry`);
1260
- reply = await this.autoRetryUntilComplete({
1261
- chatId,
1262
- sessionKey,
1263
- senderName: lastHuman.senderName,
1264
- triggerId,
1265
- lastHumanMessageId: lastHuman.messageId || "",
1266
- isNativeCommandTrigger,
1267
- initialError: errText,
1268
- liveStatus,
1269
- });
1270
- }
1271
- else {
1272
- throw mainErr;
1273
- }
1247
+ // The main run failed before returning a reply. Surface the error; we
1248
+ // only auto-retry replies that look truncated, not transient run errors.
1249
+ throw mainErr;
1274
1250
  }
1275
1251
  finally {
1276
1252
  // OpenClaw may emit the final assistant session.message just after
@@ -1670,21 +1646,6 @@ export class FeishuBot {
1670
1646
  isRuntimeFailureText(text) {
1671
1647
  return text.startsWith("⚠️ Agent 未正常完成") || /\n原因:\s*rpc\b/.test(text);
1672
1648
  }
1673
- /**
1674
- * Is this main-run error worth auto-retrying (by resuming the session via the
1675
- * probe loop)? Mainly transient failures — request/response timeouts and
1676
- * aborted/incomplete runs. We do NOT auto-retry clearly terminal errors
1677
- * (auth/permission/quota/billing/model-not-supported), which would just fail
1678
- * again. The error text comes from collectReply's `Agent error: <msg>`.
1679
- */
1680
- isRetryableAgentError(errText) {
1681
- const t = (errText || "").toLowerCase();
1682
- // Terminal errors: never auto-retry.
1683
- if (/(unauthorized|permission|forbidden|invalid api key|quota|billing|insufficient|model_not_supported|not supported|invalid_request)/.test(t))
1684
- return false;
1685
- // Retryable: timeouts and transient interruptions.
1686
- return /(timed out|timeout|aborted|operation was aborted|no response|request failed|temporarily|connection|econnreset|socket hang|stream)/.test(t);
1687
- }
1688
1649
  /**
1689
1650
  * Heuristic: does this reply look truncated / mid-task? Bias toward false
1690
1651
  * positives (“宁可错杀不放过”) — a false positive only costs one invisible
@@ -1808,21 +1769,16 @@ export class FeishuBot {
1808
1769
  * session kept working and its new output becomes the latest reply, which we
1809
1770
  * re-check. Loops until the session confirms completion, a reply no longer
1810
1771
  * looks truncated, the budget is spent, or the session cannot answer.
1772
+ *
1773
+ * This is the ONLY auto-retry trigger: a reply that LOOKS UNFINISHED. We do not
1774
+ * retry on transient run errors, and we do not auto-compact-and-retry.
1811
1775
  */
1812
1776
  async autoRetryUntilComplete(params) {
1813
1777
  const { chatId, sessionKey, senderName, triggerId, lastHumanMessageId, isNativeCommandTrigger, liveStatus } = params;
1814
1778
  let reply = params.initialReply ?? "";
1815
- // Whether we still owe at least one probe regardless of how `reply` looks.
1816
- // The timeout path starts in this state (no reply to inspect yet).
1817
- let forceProbe = params.initialError !== undefined && !params.initialReply;
1818
- if (!autoRetryEnabled() || isNativeCommandTrigger) {
1819
- if (forceProbe)
1820
- throw new Error(`Agent error: ${params.initialError}`);
1779
+ if (!autoRetryEnabled() || isNativeCommandTrigger)
1821
1780
  return reply;
1822
- }
1823
1781
  const maxRetry = autoRetryMax();
1824
- let compacted = false; // auto-compact at most once per auto-retry round
1825
- let probedAtLeastOnce = false;
1826
1782
  // Snapshot the stop generation at entry. If the user runs /stop while this
1827
1783
  // loop is in flight, handleStopCommand bumps the epoch; we detect the change
1828
1784
  // and stop retrying immediately instead of issuing more probes.
@@ -1832,26 +1788,21 @@ export class FeishuBot {
1832
1788
  // /stop while we were looping: abandon retries and deliver what we have.
1833
1789
  if (wasStopped()) {
1834
1790
  console.log(`[${this.config.name}] auto-retry: aborted by /stop for ${chatId.slice(-8)}`);
1835
- if (!reply)
1836
- throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
1837
1791
  return reply;
1838
1792
  }
1839
- if (!forceProbe) {
1840
- const parsed = this.extractBridgeAttachments(reply);
1841
- const trimmed = parsed.text.trim();
1842
- // Hard rule: a reply that produced attachments (image/file/doc) is a
1843
- // finished product — never auto-retry it (and never risk dropping the
1844
- // bridge attachment marker by replacing `reply` with a probe answer).
1845
- if (parsed.attachments.length > 0)
1846
- return reply;
1847
- // Only engage for plausibly-successful-but-truncated text. Empty/NO_REPLY
1848
- // and runtime failures have their own handling upstream.
1849
- if (!trimmed || trimmed.toUpperCase() === "NO_REPLY" || this.isRuntimeFailureText(trimmed))
1850
- return reply;
1851
- if (!this.looksTruncated(trimmed))
1852
- return reply;
1853
- }
1854
- forceProbe = false; // consumed; subsequent rounds inspect the probe reply
1793
+ const parsed = this.extractBridgeAttachments(reply);
1794
+ const trimmed = parsed.text.trim();
1795
+ // Hard rule: a reply that produced attachments (image/file/doc) is a
1796
+ // finished product — never auto-retry it (and never risk dropping the
1797
+ // bridge attachment marker by replacing `reply` with a probe answer).
1798
+ if (parsed.attachments.length > 0)
1799
+ return reply;
1800
+ // Only engage for plausibly-successful-but-truncated text. Empty/NO_REPLY
1801
+ // and runtime failures have their own handling upstream.
1802
+ if (!trimmed || trimmed.toUpperCase() === "NO_REPLY" || this.isRuntimeFailureText(trimmed))
1803
+ return reply;
1804
+ if (!this.looksTruncated(trimmed))
1805
+ return reply;
1855
1806
  const en = this.isEn(chatId);
1856
1807
  await liveStatus?.progress(en
1857
1808
  ? `⚠️ Reply looks incomplete — auto-checking (${attempt}/${maxRetry})…`
@@ -1875,41 +1826,11 @@ export class FeishuBot {
1875
1826
  includeBridgeAttachmentHint: false,
1876
1827
  onProgress: (event) => liveStatus?.progress(event),
1877
1828
  }));
1878
- probedAtLeastOnce = true;
1879
1829
  }
1880
1830
  catch (err) {
1881
- // Confirmation round failed (timeout/unhealthy). If the session is heavy
1882
- // (≥ threshold) and we have not compacted yet this round, auto-compact and
1883
- // keep retrying — a bloated session is the usual cause of these timeouts.
1884
- // Otherwise stop looping.
1831
+ // The confirmation round itself failed (timeout/unhealthy). Do not spin:
1832
+ // deliver the existing (truncated-looking) reply as-is.
1885
1833
  console.warn(`[${this.config.name}] auto-retry probe failed for ${chatId.slice(-8)}:`, this.errorSummary(err));
1886
- if (!compacted) {
1887
- const pct = await this.getContextUsagePercent(sessionKey);
1888
- if (pct >= autoRetryCompactPct()) {
1889
- const en = this.isEn(chatId);
1890
- await liveStatus?.progress(en
1891
- ? `🧹 Context is large (${pct}%) and the retry failed — auto-compacting…`
1892
- : `🧹 上下文过大(${pct}%)且重试出错,正在自动压缩…`).catch(() => { });
1893
- try {
1894
- const r = await this.compactWithFallback(sessionKey);
1895
- compacted = true; // mark as attempted regardless, to avoid re-compacting in a loop
1896
- if (r.compacted) {
1897
- console.log(`[${this.config.name}] auto-retry: compacted session (${r.method}${r.detail ? " " + r.detail : ""}) at ${pct}% for ${chatId.slice(-8)}; continuing`);
1898
- forceProbe = !reply; // if we still have no reply (timeout path), keep probing
1899
- continue; // retry the probe on the now-lighter session
1900
- }
1901
- // Compact was a no-op; do not spin.
1902
- console.warn(`[${this.config.name}] auto-retry: compact was a no-op for ${chatId.slice(-8)} (${r.reason || "no reason"})`);
1903
- }
1904
- catch (compactErr) {
1905
- console.warn(`[${this.config.name}] auto-retry compact failed for ${chatId.slice(-8)}:`, this.errorSummary(compactErr));
1906
- }
1907
- }
1908
- }
1909
- // No reply to fall back to (timeout path) and retries exhausted/failed:
1910
- // re-throw so the caller surfaces the original failure.
1911
- if (!reply)
1912
- throw (err instanceof Error ? err : new Error(`Agent error: ${params.initialError || "retry failed"}`));
1913
1834
  return reply;
1914
1835
  }
1915
1836
  const probeText = this.extractBridgeAttachments(probeReply).text.trim();
@@ -1917,31 +1838,20 @@ export class FeishuBot {
1917
1838
  // again. Deliver the latest meaningful output we have.
1918
1839
  if (wasStopped()) {
1919
1840
  console.log(`[${this.config.name}] auto-retry: aborted by /stop mid-probe for ${chatId.slice(-8)}`);
1920
- const latest = probeText && probeText.toUpperCase() !== "NO_REPLY" ? probeReply : reply;
1921
- if (!latest)
1922
- throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
1923
- return latest;
1841
+ return probeText && probeText.toUpperCase() !== "NO_REPLY" ? probeReply : reply;
1924
1842
  }
1925
1843
  if (!probeText || probeText.toUpperCase() === "NO_REPLY") {
1926
- if (!reply)
1927
- throw new Error(`Agent error: ${params.initialError || "no reply after retry"}`);
1928
1844
  return reply; // session could not answer; deliver existing result
1929
1845
  }
1930
1846
  if (this.isDonePhrase(probeText)) {
1931
1847
  console.log(`[${this.config.name}] auto-retry: session confirmed done after ${attempt} check(s) for ${chatId.slice(-8)}`);
1932
- // On the timeout path we have no original reply; the done confirmation
1933
- // means the interrupted work actually finished — but we have nothing
1934
- // user-visible to deliver, so treat it as NO_REPLY rather than fabricate.
1935
- return reply || "NO_REPLY";
1848
+ return reply;
1936
1849
  }
1937
1850
  // Not done: the session kept working. Its output is the new latest reply.
1938
1851
  console.log(`[${this.config.name}] auto-retry: session continued (attempt ${attempt}) for ${chatId.slice(-8)}`);
1939
1852
  reply = probeReply;
1940
1853
  }
1941
1854
  console.warn(`[${this.config.name}] auto-retry budget (${maxRetry}) spent for ${chatId.slice(-8)}; delivering latest result`);
1942
- void probedAtLeastOnce;
1943
- if (!reply)
1944
- throw new Error(`Agent error: ${params.initialError || "retry budget exhausted"}`);
1945
1855
  return reply;
1946
1856
  }
1947
1857
  cancelDelayedFailure(chatId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
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": {