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.
- package/dist/feishu-bot.d.ts +3 -48
- package/dist/feishu-bot.js +30 -215
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -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;
|
package/dist/feishu-bot.js
CHANGED
|
@@ -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.
|
|
1260
|
-
//
|
|
1261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
-
? (
|
|
1944
|
-
|
|
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.
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
-
//
|
|
1977
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|