openclaw-lark-multi-agent 1.2.3 → 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
@@ -155,46 +147,6 @@ export declare class FeishuBot {
155
147
  * A plain “no sentence-ending punctuation” is deliberately NOT sufficient.
156
148
  */
157
149
  private looksTruncated;
158
- /**
159
- * Did the model type a tool call as plain text instead of emitting a real
160
- * function call? When that happens the visible reply ends with a dangling
161
- * invoke / parameter / function_calls tag (with or without the `antml:`
162
- * prefix), because the tool-call markup leaked into the text channel and the
163
- * tool never actually ran. We detect such a tag anchored at the END of the
164
- * message — that dangling tag is the tell-tale Stephen flagged ("如果监测到
165
- * 这个结尾,说明 tool call 格式出了问题"). On detection the auto-retry loop
166
- * sends an explicit "invoke 格式错了,请重试" probe so the agent re-issues the
167
- * tool call correctly rather than the generic done-check.
168
- */
169
- /**
170
- * Did the model serialize a tool call as plain text instead of emitting a real
171
- * function call? When that happens, the structured tool-call XML leaks into the
172
- * visible text channel and the tool never runs. We must distinguish this from a
173
- * reply that merely *mentions* a tag (teaching, quoting, showing an example),
174
- * which must NOT trigger a retry. So we require strong STRUCTURAL evidence of a
175
- * leaked call, not just any tag at the end:
176
- *
177
- * 1. The reply must END with a closing `</invoke>` (optionally `antml:`).
178
- * A dangling OPEN tag or a bare `</parameter>`/`<function_calls>` alone is
179
- * not enough — too easy to hit in normal prose.
180
- * 2. Somewhere before it there must be a matching `<invoke name="...">` OPEN
181
- * tag — i.e. a whole call block (`<invoke…>…</invoke>`) materialized as
182
- * text. A lone `</invoke>` (e.g. “闭合标签是 </invoke>”) is a mention,
183
- * not a leaked call.
184
- * 3. The trailing `</invoke>` must NOT be wrapped in inline code/backticks or
185
- * a fenced code block — those are deliberate examples, not a leak.
186
- *
187
- * Anchored at the very end because a genuinely leaked call is the LAST thing in
188
- * the message (the model stopped right after “emitting” it). This keeps the
189
- * trigger tight so we do not retry on normal replies that talk about tags.
190
- */
191
- private looksLikeBrokenToolCall;
192
- /**
193
- * Is the tag at `tagIndex` inside an inline-code span (`` `…` ``) or a fenced
194
- * code block (```` ``` ````)? Such tags are deliberate examples, not a leaked
195
- * tool call, so the broken-tool-call detector must ignore them.
196
- */
197
- private isTrailingTagInCode;
198
150
  /** Current context usage percent for a session (totalTokens / contextTokens),
199
151
  * or 0 if it cannot be determined. Same basis as /status. */
200
152
  private getContextUsagePercent;
@@ -222,6 +174,9 @@ export declare class FeishuBot {
222
174
  * session kept working and its new output becomes the latest reply, which we
223
175
  * re-check. Loops until the session confirms completion, a reply no longer
224
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.
225
180
  */
226
181
  private autoRetryUntilComplete;
227
182
  private cancelDelayedFailure;
@@ -29,18 +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 the previous reply ended with a dangling tool-call tag (the model typed
33
- // an invoke/tool call as plain text and it never ran), the auto-retry probe
34
- // explicitly tells the agent its invoke format was broken and to retry, instead
35
- // of the generic done-check. It still accepts the done phrase if actually done.
36
- const AUTO_RETRY_BROKEN_TOOL_PROBE = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_BROKEN_TOOL_PROBE
37
- || `刚才的任务是不是没结束,invoke 格式错了,invoke 格式错了,请重试。如果结束了,回复“${AUTO_RETRY_DONE_PHRASE}”。`;
38
- const AUTO_RETRY_BROKEN_TOOL_PROBE_EN = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_BROKEN_TOOL_PROBE_EN
39
- || `Your last task did not finish — the invoke format was broken, the invoke format was broken, please retry. If it is actually done, reply "${AUTO_RETRY_DONE_PHRASE_EN}".`;
40
- // When an auto-retry probe errors AND context usage is at/above this percent,
41
- // auto-compact the session once and keep retrying (instead of giving up). This
42
- // rescues the “session too heavy → run times out / lock contention” failure mode.
43
- function autoRetryCompactPct() { return Number(process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_COMPACT_PCT || 50); }
44
32
  // How many of the most-recent tool calls (and their results) tool-trim keeps
45
33
  // intact — recent tool calls may still be relevant to the agent's next step.
46
34
  function toolTrimKeepRecent() { return Number(process.env.OPENCLAW_LARK_MULTI_AGENT_TOOLTRIM_KEEP_RECENT || 3); }
@@ -1256,29 +1244,9 @@ export class FeishuBot {
1256
1244
  }));
1257
1245
  }
1258
1246
  catch (mainErr) {
1259
- // The main run failed before returning a reply. If it is a retryable
1260
- // error (e.g. a request timeout) and not a session-health failure,
1261
- // reuse the SAME auto-retry mechanism to let the session resume from
1262
- // where it was interrupted, instead of giving up immediately.
1263
- if (mainErr instanceof UnhealthySessionError)
1264
- throw mainErr;
1265
- const errText = mainErr?.message || String(mainErr);
1266
- if (autoRetryEnabled() && !isNativeCommandTrigger && this.isRetryableAgentError(errText)) {
1267
- console.warn(`[${this.config.name}] main run failed (${errText.slice(0, 80)}) for ${chatId.slice(-8)}; entering auto-retry`);
1268
- reply = await this.autoRetryUntilComplete({
1269
- chatId,
1270
- sessionKey,
1271
- senderName: lastHuman.senderName,
1272
- triggerId,
1273
- lastHumanMessageId: lastHuman.messageId || "",
1274
- isNativeCommandTrigger,
1275
- initialError: errText,
1276
- liveStatus,
1277
- });
1278
- }
1279
- else {
1280
- throw mainErr;
1281
- }
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;
1282
1250
  }
1283
1251
  finally {
1284
1252
  // OpenClaw may emit the final assistant session.message just after
@@ -1678,21 +1646,6 @@ export class FeishuBot {
1678
1646
  isRuntimeFailureText(text) {
1679
1647
  return text.startsWith("⚠️ Agent 未正常完成") || /\n原因:\s*rpc\b/.test(text);
1680
1648
  }
1681
- /**
1682
- * Is this main-run error worth auto-retrying (by resuming the session via the
1683
- * probe loop)? Mainly transient failures — request/response timeouts and
1684
- * aborted/incomplete runs. We do NOT auto-retry clearly terminal errors
1685
- * (auth/permission/quota/billing/model-not-supported), which would just fail
1686
- * again. The error text comes from collectReply's `Agent error: <msg>`.
1687
- */
1688
- isRetryableAgentError(errText) {
1689
- const t = (errText || "").toLowerCase();
1690
- // Terminal errors: never auto-retry.
1691
- if (/(unauthorized|permission|forbidden|invalid api key|quota|billing|insufficient|model_not_supported|not supported|invalid_request)/.test(t))
1692
- return false;
1693
- // Retryable: timeouts and transient interruptions.
1694
- return /(timed out|timeout|aborted|operation was aborted|no response|request failed|temporarily|connection|econnreset|socket hang|stream)/.test(t);
1695
- }
1696
1649
  /**
1697
1650
  * Heuristic: does this reply look truncated / mid-task? Bias toward false
1698
1651
  * positives (“宁可错杀不放过”) — a false positive only costs one invisible
@@ -1733,76 +1686,6 @@ export class FeishuBot {
1733
1686
  return true;
1734
1687
  return false;
1735
1688
  }
1736
- /**
1737
- * Did the model type a tool call as plain text instead of emitting a real
1738
- * function call? When that happens the visible reply ends with a dangling
1739
- * invoke / parameter / function_calls tag (with or without the `antml:`
1740
- * prefix), because the tool-call markup leaked into the text channel and the
1741
- * tool never actually ran. We detect such a tag anchored at the END of the
1742
- * message — that dangling tag is the tell-tale Stephen flagged ("如果监测到
1743
- * 这个结尾,说明 tool call 格式出了问题"). On detection the auto-retry loop
1744
- * sends an explicit "invoke 格式错了,请重试" probe so the agent re-issues the
1745
- * tool call correctly rather than the generic done-check.
1746
- */
1747
- /**
1748
- * Did the model serialize a tool call as plain text instead of emitting a real
1749
- * function call? When that happens, the structured tool-call XML leaks into the
1750
- * visible text channel and the tool never runs. We must distinguish this from a
1751
- * reply that merely *mentions* a tag (teaching, quoting, showing an example),
1752
- * which must NOT trigger a retry. So we require strong STRUCTURAL evidence of a
1753
- * leaked call, not just any tag at the end:
1754
- *
1755
- * 1. The reply must END with a closing `</invoke>` (optionally `antml:`).
1756
- * A dangling OPEN tag or a bare `</parameter>`/`<function_calls>` alone is
1757
- * not enough — too easy to hit in normal prose.
1758
- * 2. Somewhere before it there must be a matching `<invoke name="...">` OPEN
1759
- * tag — i.e. a whole call block (`<invoke…>…</invoke>`) materialized as
1760
- * text. A lone `</invoke>` (e.g. “闭合标签是 </invoke>”) is a mention,
1761
- * not a leaked call.
1762
- * 3. The trailing `</invoke>` must NOT be wrapped in inline code/backticks or
1763
- * a fenced code block — those are deliberate examples, not a leak.
1764
- *
1765
- * Anchored at the very end because a genuinely leaked call is the LAST thing in
1766
- * the message (the model stopped right after “emitting” it). This keeps the
1767
- * trigger tight so we do not retry on normal replies that talk about tags.
1768
- */
1769
- looksLikeBrokenToolCall(text) {
1770
- const t = (text || "").trim();
1771
- if (!t)
1772
- return false;
1773
- // (1) Must end with a closing invoke tag (the serialized call's terminator).
1774
- const closeMatch = /<\/\s*(?:antml:)?invoke\s*>\s*$/i.exec(t);
1775
- if (!closeMatch)
1776
- return false;
1777
- // (3) Reject when that closing tag is fenced or inline-code wrapped (example).
1778
- if (this.isTrailingTagInCode(t, closeMatch.index))
1779
- return false;
1780
- // (2) Require a matching opening `<invoke ...>` before the closer — proof that
1781
- // a whole call block leaked, not just a mention of the closing tag.
1782
- const head = t.slice(0, closeMatch.index);
1783
- if (!/<\s*(?:antml:)?invoke\b[^>]*>/i.test(head))
1784
- return false;
1785
- return true;
1786
- }
1787
- /**
1788
- * Is the tag at `tagIndex` inside an inline-code span (`` `…` ``) or a fenced
1789
- * code block (```` ``` ````)? Such tags are deliberate examples, not a leaked
1790
- * tool call, so the broken-tool-call detector must ignore them.
1791
- */
1792
- isTrailingTagInCode(text, tagIndex) {
1793
- const before = text.slice(0, tagIndex);
1794
- // Fenced block: an odd number of ``` before the tag means we are inside one.
1795
- const fences = (before.match(/```/g) || []).length;
1796
- if (fences % 2 === 1)
1797
- return true;
1798
- // Inline code: an odd number of unescaped backticks on the tag's own line.
1799
- const lineStart = before.lastIndexOf("\n") + 1;
1800
- const lineBefore = before.slice(lineStart);
1801
- const ticks = (lineBefore.match(/`/g) || []).length;
1802
- if (ticks % 2 === 1)
1803
- return true;
1804
- return false;
1805
- }
1806
1689
  /** Current context usage percent for a session (totalTokens / contextTokens),
1807
1690
  * or 0 if it cannot be determined. Same basis as /status. */
1808
1691
  async getContextUsagePercent(sessionKey) {
@@ -1886,77 +1769,50 @@ export class FeishuBot {
1886
1769
  * session kept working and its new output becomes the latest reply, which we
1887
1770
  * re-check. Loops until the session confirms completion, a reply no longer
1888
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.
1889
1775
  */
1890
1776
  async autoRetryUntilComplete(params) {
1891
1777
  const { chatId, sessionKey, senderName, triggerId, lastHumanMessageId, isNativeCommandTrigger, liveStatus } = params;
1892
1778
  let reply = params.initialReply ?? "";
1893
- // Whether we still owe at least one probe regardless of how `reply` looks.
1894
- // The timeout path starts in this state (no reply to inspect yet).
1895
- let forceProbe = params.initialError !== undefined && !params.initialReply;
1896
- if (!autoRetryEnabled() || isNativeCommandTrigger) {
1897
- if (forceProbe)
1898
- throw new Error(`Agent error: ${params.initialError}`);
1779
+ if (!autoRetryEnabled() || isNativeCommandTrigger)
1899
1780
  return reply;
1900
- }
1901
1781
  const maxRetry = autoRetryMax();
1902
- let compacted = false; // auto-compact at most once per auto-retry round
1903
- let probedAtLeastOnce = false;
1904
1782
  // Snapshot the stop generation at entry. If the user runs /stop while this
1905
1783
  // loop is in flight, handleStopCommand bumps the epoch; we detect the change
1906
1784
  // and stop retrying immediately instead of issuing more probes.
1907
1785
  const startStopEpoch = this.stopEpoch.get(chatId) || 0;
1908
1786
  const wasStopped = () => (this.stopEpoch.get(chatId) || 0) !== startStopEpoch;
1909
- // Set per-iteration when the reply under inspection ended with a dangling
1910
- // tool-call tag (model typed an invoke as plain text); selects the explicit
1911
- // "invoke format was broken, retry" probe instead of the generic done-check.
1912
- let brokenToolCall = false;
1913
1787
  for (let attempt = 1; attempt <= maxRetry; attempt++) {
1914
1788
  // /stop while we were looping: abandon retries and deliver what we have.
1915
1789
  if (wasStopped()) {
1916
1790
  console.log(`[${this.config.name}] auto-retry: aborted by /stop for ${chatId.slice(-8)}`);
1917
- if (!reply)
1918
- throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
1919
1791
  return reply;
1920
1792
  }
1921
- brokenToolCall = false;
1922
- if (!forceProbe) {
1923
- const parsed = this.extractBridgeAttachments(reply);
1924
- const trimmed = parsed.text.trim();
1925
- // Hard rule: a reply that produced attachments (image/file/doc) is a
1926
- // finished product — never auto-retry it (and never risk dropping the
1927
- // bridge attachment marker by replacing `reply` with a probe answer).
1928
- if (parsed.attachments.length > 0)
1929
- return reply;
1930
- // Only engage for plausibly-successful-but-truncated text. Empty/NO_REPLY
1931
- // and runtime failures have their own handling upstream.
1932
- if (!trimmed || trimmed.toUpperCase() === "NO_REPLY" || this.isRuntimeFailureText(trimmed))
1933
- return reply;
1934
- // A dangling invoke/tool-call tag is a strong "tool call leaked as text"
1935
- // signal — engage even if the generic truncation heuristic would miss it.
1936
- brokenToolCall = this.looksLikeBrokenToolCall(trimmed);
1937
- if (!this.looksTruncated(trimmed) && !brokenToolCall)
1938
- return reply;
1939
- }
1940
- 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;
1941
1806
  const en = this.isEn(chatId);
1942
1807
  await liveStatus?.progress(en
1943
- ? (brokenToolCall
1944
- ? `⚠️ Tool-call format looked broken — asking it to retry (${attempt}/${maxRetry})…`
1945
- : `⚠️ Reply looks incomplete — auto-checking (${attempt}/${maxRetry})…`)
1946
- : (brokenToolCall
1947
- ? `⚠️ 工具调用格式疑似出错,正在让它重试(${attempt}/${maxRetry})…`
1948
- : `⚠️ 疑似任务未完成,正在自动重试(${attempt}/${maxRetry})…`)).catch(() => { });
1808
+ ? `⚠️ Reply looks incomplete — auto-checking (${attempt}/${maxRetry})…`
1809
+ : `⚠️ 疑似任务未完成,正在自动重试(${attempt}/${maxRetry})…`).catch(() => { });
1949
1810
  // One short line, in the user's own voice (we intentionally do not change
1950
1811
  // senderName). Each locale uses its own done-phrase so the English probe
1951
- // stays fully English; detection accepts both phrases. When the previous
1952
- // reply ended with a dangling tool-call tag, send the explicit "invoke
1953
- // format was broken, retry" probe instead of the generic done-check so the
1954
- // agent re-issues the tool call correctly.
1955
- const probe = brokenToolCall
1956
- ? (en ? AUTO_RETRY_BROKEN_TOOL_PROBE_EN : AUTO_RETRY_BROKEN_TOOL_PROBE)
1957
- : (en
1958
- ? `Is that done? If so just reply "${AUTO_RETRY_DONE_PHRASE_EN}", otherwise keep going.`
1959
- : `刚才那个任务结束了吗?结束了就回我“${AUTO_RETRY_DONE_PHRASE}”,没结束就接着做。`);
1812
+ // stays fully English; detection accepts both phrases.
1813
+ const probe = en
1814
+ ? `Is that done? If so just reply "${AUTO_RETRY_DONE_PHRASE_EN}", otherwise keep going.`
1815
+ : `刚才那个任务结束了吗?结束了就回我“${AUTO_RETRY_DONE_PHRASE}”,没结束就接着做。`;
1960
1816
  let probeReply;
1961
1817
  try {
1962
1818
  probeReply = await this.withSessionHealthMonitor(chatId, lastHumanMessageId, triggerId, this.openclawClient.chatSendWithContext({
@@ -1970,41 +1826,11 @@ export class FeishuBot {
1970
1826
  includeBridgeAttachmentHint: false,
1971
1827
  onProgress: (event) => liveStatus?.progress(event),
1972
1828
  }));
1973
- probedAtLeastOnce = true;
1974
1829
  }
1975
1830
  catch (err) {
1976
- // Confirmation round failed (timeout/unhealthy). If the session is heavy
1977
- // (≥ threshold) and we have not compacted yet this round, auto-compact and
1978
- // keep retrying — a bloated session is the usual cause of these timeouts.
1979
- // Otherwise stop looping.
1831
+ // The confirmation round itself failed (timeout/unhealthy). Do not spin:
1832
+ // deliver the existing (truncated-looking) reply as-is.
1980
1833
  console.warn(`[${this.config.name}] auto-retry probe failed for ${chatId.slice(-8)}:`, this.errorSummary(err));
1981
- if (!compacted) {
1982
- const pct = await this.getContextUsagePercent(sessionKey);
1983
- if (pct >= autoRetryCompactPct()) {
1984
- const en = this.isEn(chatId);
1985
- await liveStatus?.progress(en
1986
- ? `🧹 Context is large (${pct}%) and the retry failed — auto-compacting…`
1987
- : `🧹 上下文过大(${pct}%)且重试出错,正在自动压缩…`).catch(() => { });
1988
- try {
1989
- const r = await this.compactWithFallback(sessionKey);
1990
- compacted = true; // mark as attempted regardless, to avoid re-compacting in a loop
1991
- if (r.compacted) {
1992
- console.log(`[${this.config.name}] auto-retry: compacted session (${r.method}${r.detail ? " " + r.detail : ""}) at ${pct}% for ${chatId.slice(-8)}; continuing`);
1993
- forceProbe = !reply; // if we still have no reply (timeout path), keep probing
1994
- continue; // retry the probe on the now-lighter session
1995
- }
1996
- // Compact was a no-op; do not spin.
1997
- console.warn(`[${this.config.name}] auto-retry: compact was a no-op for ${chatId.slice(-8)} (${r.reason || "no reason"})`);
1998
- }
1999
- catch (compactErr) {
2000
- console.warn(`[${this.config.name}] auto-retry compact failed for ${chatId.slice(-8)}:`, this.errorSummary(compactErr));
2001
- }
2002
- }
2003
- }
2004
- // No reply to fall back to (timeout path) and retries exhausted/failed:
2005
- // re-throw so the caller surfaces the original failure.
2006
- if (!reply)
2007
- throw (err instanceof Error ? err : new Error(`Agent error: ${params.initialError || "retry failed"}`));
2008
1834
  return reply;
2009
1835
  }
2010
1836
  const probeText = this.extractBridgeAttachments(probeReply).text.trim();
@@ -2012,31 +1838,20 @@ export class FeishuBot {
2012
1838
  // again. Deliver the latest meaningful output we have.
2013
1839
  if (wasStopped()) {
2014
1840
  console.log(`[${this.config.name}] auto-retry: aborted by /stop mid-probe for ${chatId.slice(-8)}`);
2015
- const latest = probeText && probeText.toUpperCase() !== "NO_REPLY" ? probeReply : reply;
2016
- if (!latest)
2017
- throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
2018
- return latest;
1841
+ return probeText && probeText.toUpperCase() !== "NO_REPLY" ? probeReply : reply;
2019
1842
  }
2020
1843
  if (!probeText || probeText.toUpperCase() === "NO_REPLY") {
2021
- if (!reply)
2022
- throw new Error(`Agent error: ${params.initialError || "no reply after retry"}`);
2023
1844
  return reply; // session could not answer; deliver existing result
2024
1845
  }
2025
1846
  if (this.isDonePhrase(probeText)) {
2026
1847
  console.log(`[${this.config.name}] auto-retry: session confirmed done after ${attempt} check(s) for ${chatId.slice(-8)}`);
2027
- // On the timeout path we have no original reply; the done confirmation
2028
- // means the interrupted work actually finished — but we have nothing
2029
- // user-visible to deliver, so treat it as NO_REPLY rather than fabricate.
2030
- return reply || "NO_REPLY";
1848
+ return reply;
2031
1849
  }
2032
1850
  // Not done: the session kept working. Its output is the new latest reply.
2033
1851
  console.log(`[${this.config.name}] auto-retry: session continued (attempt ${attempt}) for ${chatId.slice(-8)}`);
2034
1852
  reply = probeReply;
2035
1853
  }
2036
1854
  console.warn(`[${this.config.name}] auto-retry budget (${maxRetry}) spent for ${chatId.slice(-8)}; delivering latest result`);
2037
- void probedAtLeastOnce;
2038
- if (!reply)
2039
- throw new Error(`Agent error: ${params.initialError || "retry budget exhausted"}`);
2040
1855
  return reply;
2041
1856
  }
2042
1857
  cancelDelayedFailure(chatId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.2.3",
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": {