openclaw-lark-multi-agent 1.2.4 → 1.2.6
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.
- package/dist/feishu-bot.d.ts +14 -28
- package/dist/feishu-bot.js +45 -134
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -123,36 +123,19 @@ export declare class FeishuBot {
|
|
|
123
123
|
private isLegacyBridgeControlMessage;
|
|
124
124
|
private isRuntimeFailureText;
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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
|
-
/**
|
|
134
|
-
* Heuristic: does this reply look truncated / mid-task? Bias toward false
|
|
135
|
-
* positives (“宁可错杀不放过”) — a false positive only costs one invisible
|
|
136
|
-
* confirmation round, and the session self-check is the real arbiter. We gate
|
|
137
|
-
* only on cheap text shape here, never on stopReason (the upstream rarely
|
|
138
|
-
* provides one for normal-looking-but-truncated finals).
|
|
139
|
-
*/
|
|
140
|
-
/**
|
|
141
|
-
* Heuristic: does this reply look genuinely truncated / cut off mid-task?
|
|
126
|
+
* Heuristic: does this reply look truncated / cut off mid-task? Used as the
|
|
127
|
+
* SOLE auto-retry trigger — if true, we ask the session a one-line “done?” probe.
|
|
142
128
|
*
|
|
143
|
-
*
|
|
144
|
-
* did not finish — do NOT retry just because a reply lacks a trailing period.
|
|
145
|
-
* Many complete replies legitimately end without sentence punctuation (a terse
|
|
146
|
-
* “好的”, a bullet list, a path, a number, a closing word). Treating those as
|
|
147
|
-
* truncated caused needless extra probe rounds, which hurts UX. So we gate on
|
|
148
|
-
* strong structural signals only:
|
|
129
|
+
* Signals (any one is enough):
|
|
149
130
|
* 1. Unbalanced code fence — almost certainly cut off mid code block.
|
|
150
|
-
* 2.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
131
|
+
* 2. Trailing clause-joining punctuation (comma/colon/semicolon/、) — a
|
|
132
|
+
* sentence was left hanging.
|
|
133
|
+
* 3. A dangling connector / lead-in (“然后”, “接下来”, “let me”, “and”…).
|
|
134
|
+
* 4. Ends without ANY sentence-ending punctuation / closer (no terminal
|
|
135
|
+
* punctuation, closing bracket/quote, code fence, or wrap-up emoji).
|
|
136
|
+
* Per Stephen: a reply that ends with no punctuation at all is also
|
|
137
|
+
* treated as likely unfinished. A false positive only costs one cheap,
|
|
138
|
+
* invisible confirmation probe; the session itself is the real arbiter.
|
|
156
139
|
*/
|
|
157
140
|
private looksTruncated;
|
|
158
141
|
/** Current context usage percent for a session (totalTokens / contextTokens),
|
|
@@ -182,6 +165,9 @@ export declare class FeishuBot {
|
|
|
182
165
|
* session kept working and its new output becomes the latest reply, which we
|
|
183
166
|
* re-check. Loops until the session confirms completion, a reply no longer
|
|
184
167
|
* looks truncated, the budget is spent, or the session cannot answer.
|
|
168
|
+
*
|
|
169
|
+
* This is the ONLY auto-retry trigger: a reply that LOOKS UNFINISHED. We do not
|
|
170
|
+
* retry on transient run errors, and we do not auto-compact-and-retry.
|
|
185
171
|
*/
|
|
186
172
|
private autoRetryUntilComplete;
|
|
187
173
|
private cancelDelayedFailure;
|
package/dist/feishu-bot.js
CHANGED
|
@@ -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.
|
|
1252
|
-
//
|
|
1253
|
-
|
|
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
|
|
@@ -1671,43 +1647,19 @@ export class FeishuBot {
|
|
|
1671
1647
|
return text.startsWith("⚠️ Agent 未正常完成") || /\n原因:\s*rpc\b/.test(text);
|
|
1672
1648
|
}
|
|
1673
1649
|
/**
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
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
|
-
/**
|
|
1689
|
-
* Heuristic: does this reply look truncated / mid-task? Bias toward false
|
|
1690
|
-
* positives (“宁可错杀不放过”) — a false positive only costs one invisible
|
|
1691
|
-
* confirmation round, and the session self-check is the real arbiter. We gate
|
|
1692
|
-
* only on cheap text shape here, never on stopReason (the upstream rarely
|
|
1693
|
-
* provides one for normal-looking-but-truncated finals).
|
|
1694
|
-
*/
|
|
1695
|
-
/**
|
|
1696
|
-
* Heuristic: does this reply look genuinely truncated / cut off mid-task?
|
|
1650
|
+
* Heuristic: does this reply look truncated / cut off mid-task? Used as the
|
|
1651
|
+
* SOLE auto-retry trigger — if true, we ask the session a one-line “done?” probe.
|
|
1697
1652
|
*
|
|
1698
|
-
*
|
|
1699
|
-
* did not finish — do NOT retry just because a reply lacks a trailing period.
|
|
1700
|
-
* Many complete replies legitimately end without sentence punctuation (a terse
|
|
1701
|
-
* “好的”, a bullet list, a path, a number, a closing word). Treating those as
|
|
1702
|
-
* truncated caused needless extra probe rounds, which hurts UX. So we gate on
|
|
1703
|
-
* strong structural signals only:
|
|
1653
|
+
* Signals (any one is enough):
|
|
1704
1654
|
* 1. Unbalanced code fence — almost certainly cut off mid code block.
|
|
1705
|
-
* 2.
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1655
|
+
* 2. Trailing clause-joining punctuation (comma/colon/semicolon/、) — a
|
|
1656
|
+
* sentence was left hanging.
|
|
1657
|
+
* 3. A dangling connector / lead-in (“然后”, “接下来”, “let me”, “and”…).
|
|
1658
|
+
* 4. Ends without ANY sentence-ending punctuation / closer (no terminal
|
|
1659
|
+
* punctuation, closing bracket/quote, code fence, or wrap-up emoji).
|
|
1660
|
+
* Per Stephen: a reply that ends with no punctuation at all is also
|
|
1661
|
+
* treated as likely unfinished. A false positive only costs one cheap,
|
|
1662
|
+
* invisible confirmation probe; the session itself is the real arbiter.
|
|
1711
1663
|
*/
|
|
1712
1664
|
looksTruncated(text) {
|
|
1713
1665
|
const t = (text || "").trim();
|
|
@@ -1723,6 +1675,16 @@ export class FeishuBot {
|
|
|
1723
1675
|
// (2b) Ends on an explicit continuation cue / lead-in.
|
|
1724
1676
|
if (/(接下来|然后|首先|现在我|让我|下一步|我先|我来|我接着|接着|稍等|let me|now i|next|first|\b(?:and|then|so|to|the|a)\b)\s*$/iu.test(t))
|
|
1725
1677
|
return true;
|
|
1678
|
+
// (2c) Ends without ANY sentence-ending punctuation / closer. A finished
|
|
1679
|
+
// reply normally ends with terminal punctuation, a closing bracket/
|
|
1680
|
+
// quote, a code fence, or a wrap-up emoji; if none of those are present
|
|
1681
|
+
// the reply was most likely cut off mid-sentence. (Stephen: “结尾没有
|
|
1682
|
+
// 任何标点符号也算疑似未结束”.)
|
|
1683
|
+
const endOk = /[。!?.!?…)\]」』】)"'`”’~~—✨✅👍🙏🎉🙌]\s*$/u.test(t)
|
|
1684
|
+
|| /```\s*$/.test(t)
|
|
1685
|
+
|| /[}\]]\s*$/.test(t);
|
|
1686
|
+
if (!endOk)
|
|
1687
|
+
return true;
|
|
1726
1688
|
return false;
|
|
1727
1689
|
}
|
|
1728
1690
|
/** Current context usage percent for a session (totalTokens / contextTokens),
|
|
@@ -1808,21 +1770,16 @@ export class FeishuBot {
|
|
|
1808
1770
|
* session kept working and its new output becomes the latest reply, which we
|
|
1809
1771
|
* re-check. Loops until the session confirms completion, a reply no longer
|
|
1810
1772
|
* looks truncated, the budget is spent, or the session cannot answer.
|
|
1773
|
+
*
|
|
1774
|
+
* This is the ONLY auto-retry trigger: a reply that LOOKS UNFINISHED. We do not
|
|
1775
|
+
* retry on transient run errors, and we do not auto-compact-and-retry.
|
|
1811
1776
|
*/
|
|
1812
1777
|
async autoRetryUntilComplete(params) {
|
|
1813
1778
|
const { chatId, sessionKey, senderName, triggerId, lastHumanMessageId, isNativeCommandTrigger, liveStatus } = params;
|
|
1814
1779
|
let reply = params.initialReply ?? "";
|
|
1815
|
-
|
|
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}`);
|
|
1780
|
+
if (!autoRetryEnabled() || isNativeCommandTrigger)
|
|
1821
1781
|
return reply;
|
|
1822
|
-
}
|
|
1823
1782
|
const maxRetry = autoRetryMax();
|
|
1824
|
-
let compacted = false; // auto-compact at most once per auto-retry round
|
|
1825
|
-
let probedAtLeastOnce = false;
|
|
1826
1783
|
// Snapshot the stop generation at entry. If the user runs /stop while this
|
|
1827
1784
|
// loop is in flight, handleStopCommand bumps the epoch; we detect the change
|
|
1828
1785
|
// and stop retrying immediately instead of issuing more probes.
|
|
@@ -1832,26 +1789,21 @@ export class FeishuBot {
|
|
|
1832
1789
|
// /stop while we were looping: abandon retries and deliver what we have.
|
|
1833
1790
|
if (wasStopped()) {
|
|
1834
1791
|
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
1792
|
return reply;
|
|
1838
1793
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
return reply;
|
|
1853
|
-
}
|
|
1854
|
-
forceProbe = false; // consumed; subsequent rounds inspect the probe reply
|
|
1794
|
+
const parsed = this.extractBridgeAttachments(reply);
|
|
1795
|
+
const trimmed = parsed.text.trim();
|
|
1796
|
+
// Hard rule: a reply that produced attachments (image/file/doc) is a
|
|
1797
|
+
// finished product — never auto-retry it (and never risk dropping the
|
|
1798
|
+
// bridge attachment marker by replacing `reply` with a probe answer).
|
|
1799
|
+
if (parsed.attachments.length > 0)
|
|
1800
|
+
return reply;
|
|
1801
|
+
// Only engage for plausibly-successful-but-truncated text. Empty/NO_REPLY
|
|
1802
|
+
// and runtime failures have their own handling upstream.
|
|
1803
|
+
if (!trimmed || trimmed.toUpperCase() === "NO_REPLY" || this.isRuntimeFailureText(trimmed))
|
|
1804
|
+
return reply;
|
|
1805
|
+
if (!this.looksTruncated(trimmed))
|
|
1806
|
+
return reply;
|
|
1855
1807
|
const en = this.isEn(chatId);
|
|
1856
1808
|
await liveStatus?.progress(en
|
|
1857
1809
|
? `⚠️ Reply looks incomplete — auto-checking (${attempt}/${maxRetry})…`
|
|
@@ -1875,41 +1827,11 @@ export class FeishuBot {
|
|
|
1875
1827
|
includeBridgeAttachmentHint: false,
|
|
1876
1828
|
onProgress: (event) => liveStatus?.progress(event),
|
|
1877
1829
|
}));
|
|
1878
|
-
probedAtLeastOnce = true;
|
|
1879
1830
|
}
|
|
1880
1831
|
catch (err) {
|
|
1881
|
-
//
|
|
1882
|
-
//
|
|
1883
|
-
// keep retrying — a bloated session is the usual cause of these timeouts.
|
|
1884
|
-
// Otherwise stop looping.
|
|
1832
|
+
// The confirmation round itself failed (timeout/unhealthy). Do not spin:
|
|
1833
|
+
// deliver the existing (truncated-looking) reply as-is.
|
|
1885
1834
|
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
1835
|
return reply;
|
|
1914
1836
|
}
|
|
1915
1837
|
const probeText = this.extractBridgeAttachments(probeReply).text.trim();
|
|
@@ -1917,31 +1839,20 @@ export class FeishuBot {
|
|
|
1917
1839
|
// again. Deliver the latest meaningful output we have.
|
|
1918
1840
|
if (wasStopped()) {
|
|
1919
1841
|
console.log(`[${this.config.name}] auto-retry: aborted by /stop mid-probe for ${chatId.slice(-8)}`);
|
|
1920
|
-
|
|
1921
|
-
if (!latest)
|
|
1922
|
-
throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
|
|
1923
|
-
return latest;
|
|
1842
|
+
return probeText && probeText.toUpperCase() !== "NO_REPLY" ? probeReply : reply;
|
|
1924
1843
|
}
|
|
1925
1844
|
if (!probeText || probeText.toUpperCase() === "NO_REPLY") {
|
|
1926
|
-
if (!reply)
|
|
1927
|
-
throw new Error(`Agent error: ${params.initialError || "no reply after retry"}`);
|
|
1928
1845
|
return reply; // session could not answer; deliver existing result
|
|
1929
1846
|
}
|
|
1930
1847
|
if (this.isDonePhrase(probeText)) {
|
|
1931
1848
|
console.log(`[${this.config.name}] auto-retry: session confirmed done after ${attempt} check(s) for ${chatId.slice(-8)}`);
|
|
1932
|
-
|
|
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";
|
|
1849
|
+
return reply;
|
|
1936
1850
|
}
|
|
1937
1851
|
// Not done: the session kept working. Its output is the new latest reply.
|
|
1938
1852
|
console.log(`[${this.config.name}] auto-retry: session continued (attempt ${attempt}) for ${chatId.slice(-8)}`);
|
|
1939
1853
|
reply = probeReply;
|
|
1940
1854
|
}
|
|
1941
1855
|
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
1856
|
return reply;
|
|
1946
1857
|
}
|
|
1947
1858
|
cancelDelayedFailure(chatId) {
|