openclaw-lark-multi-agent 1.4.13 → 1.5.1
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/compact-progress.d.ts +3 -1
- package/dist/compact-progress.js +6 -0
- package/dist/config.d.ts +2 -0
- package/dist/feishu-bot.d.ts +21 -11
- package/dist/feishu-bot.js +260 -63
- package/dist/live-status.d.ts +5 -0
- package/dist/live-status.js +2 -0
- package/dist/openclaw-client.d.ts +8 -0
- package/dist/openclaw-client.js +57 -2
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Coarse phase of the compaction flow, drives the card's primary line. */
|
|
2
|
-
export type CompactPhase = "native" | "transcript-trim";
|
|
2
|
+
export type CompactPhase = "native" | "transcript-trim" | "trim";
|
|
3
3
|
/** Terminal outcome used by the renderer to pick color/emoji. */
|
|
4
4
|
export type CompactState = "running" | "done" | "failed" | "noop";
|
|
5
5
|
/** Structured snapshot the renderer turns into a Feishu interactive card. */
|
|
@@ -50,6 +50,8 @@ export declare class CompactProgressController {
|
|
|
50
50
|
get id(): string | undefined;
|
|
51
51
|
/** Begin the flow; schedules lazy card creation after delayMs. */
|
|
52
52
|
start(): void;
|
|
53
|
+
/** Switch straight into explicit transcript-trim mode (user-requested trim). */
|
|
54
|
+
toTrim(): void;
|
|
53
55
|
/** Native compaction gave up; switch to Gateway-owned transcript-tail trim. */
|
|
54
56
|
toTranscriptTrim(): Promise<void>;
|
|
55
57
|
/** Finished successfully; patch the card to the done state with a detail line. */
|
package/dist/compact-progress.js
CHANGED
|
@@ -45,6 +45,12 @@ export class CompactProgressController {
|
|
|
45
45
|
}, this.opts.delayMs ?? DEFAULT_DELAY_MS);
|
|
46
46
|
this.createTimer.unref?.();
|
|
47
47
|
}
|
|
48
|
+
/** Switch straight into explicit transcript-trim mode (user-requested trim). */
|
|
49
|
+
toTrim() {
|
|
50
|
+
if (this.finalized || this.phase === "trim" || this.phase === "transcript-trim")
|
|
51
|
+
return;
|
|
52
|
+
this.phase = "trim";
|
|
53
|
+
}
|
|
48
54
|
/** Native compaction gave up; switch to Gateway-owned transcript-tail trim. */
|
|
49
55
|
async toTranscriptTrim() {
|
|
50
56
|
if (this.finalized || this.phase === "transcript-trim")
|
package/dist/config.d.ts
CHANGED
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -219,6 +219,10 @@ export declare class FeishuBot {
|
|
|
219
219
|
private isBotAvailableInChat;
|
|
220
220
|
private getDiscussionParticipants;
|
|
221
221
|
private getChairmanParticipant;
|
|
222
|
+
/** Resolve the session's current thinking label for the live-status footer.
|
|
223
|
+
* Prefers a live session override (set via //think / sessions.patch), falls
|
|
224
|
+
* back to the session's default, then the bot's config-level thinking. */
|
|
225
|
+
private fetchThinkingLabel;
|
|
222
226
|
private asDiscussionParticipant;
|
|
223
227
|
private runDiscussionTurn;
|
|
224
228
|
private buildLiveStatusCard;
|
|
@@ -286,6 +290,15 @@ export declare class FeishuBot {
|
|
|
286
290
|
*/
|
|
287
291
|
private sendErrorNotice;
|
|
288
292
|
private sendFinalMessage;
|
|
293
|
+
/** Feishu code 230025: message content over the per-message limit. */
|
|
294
|
+
private isContentTooLongError;
|
|
295
|
+
/** Split text into Feishu-safe chunks (default well under the 4000-char text limit).
|
|
296
|
+
* Prefers paragraph, then line boundaries; hard-cuts only when a single line
|
|
297
|
+
* is longer than the limit. Joining all chunks reproduces the original text. */
|
|
298
|
+
private splitIntoChunks;
|
|
299
|
+
/** Deliver oversized content as multiple plain-text messages (used on 230025).
|
|
300
|
+
* Keeps the model footer on the final chunk. Returns the first message id. */
|
|
301
|
+
private sendChunkedText;
|
|
289
302
|
private sendMessage;
|
|
290
303
|
/**
|
|
291
304
|
* Enqueue a message send to guarantee ordering per chat.
|
|
@@ -316,14 +329,15 @@ export declare class FeishuBot {
|
|
|
316
329
|
* Handle /compact command: compress session context.
|
|
317
330
|
*/
|
|
318
331
|
/**
|
|
319
|
-
* Compact a session
|
|
320
|
-
*
|
|
321
|
-
* 2. If summarization cannot run, ask the same RPC to retain only a bounded
|
|
322
|
-
* transcript tail via `maxLines`. This works with legacy JSONL and the
|
|
323
|
-
* SQLite session store introduced in OpenClaw 2026.8.x, and preserves
|
|
324
|
-
* OpenClaw's write locks, transcript invariants, and migration ownership.
|
|
332
|
+
* Compact a session via Gateway-owned RPCs. LMA deliberately never opens or
|
|
333
|
+
* rewrites OpenClaw transcript storage.
|
|
325
334
|
*
|
|
326
|
-
*
|
|
335
|
+
* Modes (kept strictly separate so the flows are never mixed):
|
|
336
|
+
* - "trim" (default): `sessions.compact` with `maxLines` — permanently keeps
|
|
337
|
+
* only the last N transcript events. Model-free, fast, never times out on
|
|
338
|
+
* oversized sessions.
|
|
339
|
+
* - "semantic": LLM summarization via the same RPC; on failure falls back to
|
|
340
|
+
* a trim so the session is still reclaimed.
|
|
327
341
|
*/
|
|
328
342
|
private compactWithFallback;
|
|
329
343
|
private handleCompactCommand;
|
|
@@ -344,9 +358,6 @@ export declare class FeishuBot {
|
|
|
344
358
|
* Called once per chat on first message.
|
|
345
359
|
*/
|
|
346
360
|
private fetchAndCacheChatInfo;
|
|
347
|
-
/**
|
|
348
|
-
* Send a model-drift notification to the affected chat.
|
|
349
|
-
*/
|
|
350
361
|
private hydrateInlineImageKeys;
|
|
351
362
|
private hydrateFeishuDocsInMessage;
|
|
352
363
|
private extractFeishuDocRefs;
|
|
@@ -365,5 +376,4 @@ export declare class FeishuBot {
|
|
|
365
376
|
* Extract text content from a rich text (post) message.
|
|
366
377
|
*/
|
|
367
378
|
private extractPostText;
|
|
368
|
-
private notifyModelDrift;
|
|
369
379
|
}
|
package/dist/feishu-bot.js
CHANGED
|
@@ -37,6 +37,40 @@ const AUTO_RETRY_DONE_PHRASE_EN = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RET
|
|
|
37
37
|
// it is a NEW user message that arrived while it was still working, and should
|
|
38
38
|
// be handled together with the current task. The live-status card and reactions
|
|
39
39
|
// still key off the ORIGINAL text; only what OpenClaw receives is wrapped.
|
|
40
|
+
/** Damerau-Levenshtein distance (insert/delete/substitute/transpose), bounded. */
|
|
41
|
+
function damerauLevenshtein(a, b) {
|
|
42
|
+
const la = a.length, lb = b.length;
|
|
43
|
+
if (Math.abs(la - lb) > 1)
|
|
44
|
+
return 2; // fast reject for threshold-1 use
|
|
45
|
+
const d = Array.from({ length: la + 1 }, () => new Array(lb + 1).fill(0));
|
|
46
|
+
for (let i = 0; i <= la; i++)
|
|
47
|
+
d[i][0] = i;
|
|
48
|
+
for (let j = 0; j <= lb; j++)
|
|
49
|
+
d[0][j] = j;
|
|
50
|
+
for (let i = 1; i <= la; i++) {
|
|
51
|
+
for (let j = 1; j <= lb; j++) {
|
|
52
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
53
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
54
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
55
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return d[la][lb];
|
|
60
|
+
}
|
|
61
|
+
/** Find a bridge command within one edit of `name` (typo hint); undefined if none. */
|
|
62
|
+
function findNearMissCommand(name, commands) {
|
|
63
|
+
if (!name.startsWith("/") || name.length < 3)
|
|
64
|
+
return undefined;
|
|
65
|
+
let best;
|
|
66
|
+
for (const cmd of commands) {
|
|
67
|
+
if (damerauLevenshtein(name, cmd) <= 1) {
|
|
68
|
+
if (best === undefined || cmd.length < best.length)
|
|
69
|
+
best = cmd;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return best;
|
|
73
|
+
}
|
|
40
74
|
function steerInjectionPrefix(en) {
|
|
41
75
|
const zh = process.env.OPENCLAW_LARK_MULTI_AGENT_STEER_PREFIX
|
|
42
76
|
|| "【用户中途插入的新消息,请与当前任务一起处理】";
|
|
@@ -44,12 +78,12 @@ function steerInjectionPrefix(en) {
|
|
|
44
78
|
|| "[New message inserted by the user mid-task; please handle it together with the current task]";
|
|
45
79
|
return en ? enText : zh;
|
|
46
80
|
}
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
81
|
+
// Default transcript-tail trim depth for /compact. Tool-heavy group sessions can
|
|
82
|
+
// easily carry 300+ events of pure tool noise, so keep a generous tail: the
|
|
83
|
+
// Gateway truncates to the last N transcript events. Env override stays.
|
|
50
84
|
function compactFallbackMaxLines() {
|
|
51
|
-
const value = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_FALLBACK_MAX_LINES ||
|
|
52
|
-
return Number.isFinite(value) ? Math.max(20, Math.floor(value)) :
|
|
85
|
+
const value = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_FALLBACK_MAX_LINES || 800);
|
|
86
|
+
return Number.isFinite(value) ? Math.max(20, Math.floor(value)) : 800;
|
|
53
87
|
}
|
|
54
88
|
// After a reply is delivered, if context usage reaches this percent, send the
|
|
55
89
|
// user a one-time alert to compact. 0 disables the alert.
|
|
@@ -212,7 +246,6 @@ export class FeishuBot {
|
|
|
212
246
|
const corrected = await this.openclawClient.ensureModel(sessionKey, this.config.model);
|
|
213
247
|
if (corrected) {
|
|
214
248
|
console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
|
|
215
|
-
await this.notifyModelDrift(chatId, sessionKey);
|
|
216
249
|
}
|
|
217
250
|
return sessionKey;
|
|
218
251
|
}
|
|
@@ -225,7 +258,6 @@ export class FeishuBot {
|
|
|
225
258
|
const corrected = await this.openclawClient.ensureModel(sessionKey, this.config.model);
|
|
226
259
|
if (corrected) {
|
|
227
260
|
console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
|
|
228
|
-
await this.notifyModelDrift(chatId, sessionKey);
|
|
229
261
|
}
|
|
230
262
|
}
|
|
231
263
|
else {
|
|
@@ -572,6 +604,28 @@ export class FeishuBot {
|
|
|
572
604
|
const commandName = preliminaryCommandName;
|
|
573
605
|
const bridgeCommands = preliminaryBridgeCommands;
|
|
574
606
|
const isCommand = isBridgeCommand && bridgeCommands.has(commandName);
|
|
607
|
+
// Typo hint: a near-miss single-slash command (e.g. "/chiarman") must not
|
|
608
|
+
// silently fall through into the chairman/model chat path — that is how
|
|
609
|
+
// "I set the chairman but someone else keeps answering" confusion starts.
|
|
610
|
+
if (!isCommand && isBridgeCommand) {
|
|
611
|
+
const nearMiss = findNearMissCommand(commandName, bridgeCommands);
|
|
612
|
+
if (nearMiss) {
|
|
613
|
+
const groupBotCount = Array.from(FeishuBot.allBots.values()).filter((bot) => bot.store === this.store).length;
|
|
614
|
+
const shouldHint = chatType === "p2p"
|
|
615
|
+
|| (routing.hasTargetedMention ? routing.isCurrentBotMentioned : (groupBotCount === 1 || this.isDiscussionCoordinator()));
|
|
616
|
+
if (shouldHint) {
|
|
617
|
+
const en = this.isEn(chatId);
|
|
618
|
+
await this.replyMessage(messageId, en
|
|
619
|
+
? `❓ Unknown command \`${commandName}\`. Did you mean \`${nearMiss}\`?\nRe-send the correct command to execute it. Commands starting with // are passed to OpenClaw.`
|
|
620
|
+
: `❓ 未知命令 \`${commandName}\`,你是想输入 \`${nearMiss}\` 吗?\n重新发送正确命令即可执行;双斜杠 // 开头的命令会转交给 OpenClaw。`);
|
|
621
|
+
if (insertedId > 0) {
|
|
622
|
+
this.store.markSynced(this.config.name, chatId, insertedId);
|
|
623
|
+
this.store.clearPendingTrigger(this.config.name, chatId, insertedId);
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
575
629
|
if (isCommand) {
|
|
576
630
|
// In group chats, most bridge commands must be explicitly routed to this
|
|
577
631
|
// bot or @all. /discuss is a group-level command, so an unmentioned
|
|
@@ -683,7 +737,7 @@ export class FeishuBot {
|
|
|
683
737
|
`━━━━━━━━━━━━━━━━━━`,
|
|
684
738
|
`桥接层命令(单斜杠,由 openclaw-lark-multi-agent 本地处理)`,
|
|
685
739
|
`📊 /status — 查看当前模型、Token 用量、Session 状态`,
|
|
686
|
-
`🧹 /compact —
|
|
740
|
+
`🧹 /compact [N|maxLines=N|semantic] — 裁剪转录尾部(默认,保留最近事件);N 指定保留条数;semantic 走 LLM 总结`,
|
|
687
741
|
`🔄 /reset — 重置当前 bot 的 OpenClaw session`,
|
|
688
742
|
`⏹️ /stop — 强制停止当前 bot 在本聊天的卡死 run,解锁队列`,
|
|
689
743
|
`🔊 /verbose — 开关当前聊天里的 Tool Call 显示`,
|
|
@@ -729,7 +783,8 @@ export class FeishuBot {
|
|
|
729
783
|
}
|
|
730
784
|
if (commandName === "/compact") {
|
|
731
785
|
await this.ensureSession(chatId);
|
|
732
|
-
|
|
786
|
+
const compactArgs = cleanText.trim().split(/\s+/).slice(1).join(" ").trim();
|
|
787
|
+
await this.handleCompactCommand(chatId, messageId, compactArgs);
|
|
733
788
|
markCommandSynced();
|
|
734
789
|
return;
|
|
735
790
|
}
|
|
@@ -1360,6 +1415,7 @@ export class FeishuBot {
|
|
|
1360
1415
|
}, {
|
|
1361
1416
|
botName: this.config.name,
|
|
1362
1417
|
model: this.config.model,
|
|
1418
|
+
thinkingLevel: await this.fetchThinkingLabel(sessionKey),
|
|
1363
1419
|
locale: this.isEn(chatId) ? "en" : "zh",
|
|
1364
1420
|
})
|
|
1365
1421
|
: undefined;
|
|
@@ -1888,7 +1944,12 @@ export class FeishuBot {
|
|
|
1888
1944
|
const core = en
|
|
1889
1945
|
? `${emoji} ${meta.toolCalls} tool call${meta.toolCalls === 1 ? "" : "s"} · ⏱ ${meta.elapsed}`
|
|
1890
1946
|
: `${emoji} 累计${meta.toolCalls} 次工具调用 · ⏱ 耗时${meta.elapsed}`;
|
|
1891
|
-
|
|
1947
|
+
if (!meta.model?.trim())
|
|
1948
|
+
return core;
|
|
1949
|
+
const modelLabel = meta.thinkingLevel?.trim()
|
|
1950
|
+
? `${meta.model.trim()} · ${meta.thinkingLevel.trim()}`
|
|
1951
|
+
: meta.model.trim();
|
|
1952
|
+
return `${core} · 🧠 ${this.escapeCardText(modelLabel)}`;
|
|
1892
1953
|
}
|
|
1893
1954
|
/** Best-effort terminal cleanup for status-only rows and legacy v1.4.3
|
|
1894
1955
|
* outbox entries that once targeted the live-status card. */
|
|
@@ -1898,6 +1959,7 @@ export class FeishuBot {
|
|
|
1898
1959
|
lines: [],
|
|
1899
1960
|
elapsed: meta.elapsed,
|
|
1900
1961
|
model: meta.model,
|
|
1962
|
+
thinkingLevel: meta.thinkingLevel,
|
|
1901
1963
|
toolCalls: meta.toolCalls,
|
|
1902
1964
|
noReply: Boolean(meta.noReply),
|
|
1903
1965
|
state: "done",
|
|
@@ -2783,6 +2845,24 @@ export class FeishuBot {
|
|
|
2783
2845
|
const bot = Array.from(FeishuBot.allBots.values()).find((candidate) => candidate.store === this.store && candidate.config.name === chairman && this.isBotAvailableInChat(candidate, chatId));
|
|
2784
2846
|
return bot ? this.asDiscussionParticipant(bot, chatId) : undefined;
|
|
2785
2847
|
}
|
|
2848
|
+
/** Resolve the session's current thinking label for the live-status footer.
|
|
2849
|
+
* Prefers a live session override (set via //think / sessions.patch), falls
|
|
2850
|
+
* back to the session's default, then the bot's config-level thinking. */
|
|
2851
|
+
async fetchThinkingLabel(sessionKey) {
|
|
2852
|
+
try {
|
|
2853
|
+
const info = await this.openclawClient.getSessionInfo(sessionKey);
|
|
2854
|
+
const s = info?.session;
|
|
2855
|
+
const label = (typeof s?.thinkingLevel === "string" && s.thinkingLevel.trim())
|
|
2856
|
+
|| (typeof s?.thinkingDefault === "string" && s.thinkingDefault.trim())
|
|
2857
|
+
|| (typeof this.config.thinking === "string" && this.config.thinking.trim())
|
|
2858
|
+
|| "";
|
|
2859
|
+
return label || undefined;
|
|
2860
|
+
}
|
|
2861
|
+
catch {
|
|
2862
|
+
const cfg = typeof this.config.thinking === "string" ? this.config.thinking.trim() : "";
|
|
2863
|
+
return cfg || undefined;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2786
2866
|
asDiscussionParticipant(bot, chatId) {
|
|
2787
2867
|
return {
|
|
2788
2868
|
name: bot.config.name,
|
|
@@ -2801,6 +2881,7 @@ export class FeishuBot {
|
|
|
2801
2881
|
}, {
|
|
2802
2882
|
botName: this.config.name,
|
|
2803
2883
|
model: this.config.model,
|
|
2884
|
+
thinkingLevel: await this.fetchThinkingLabel(sessionKey),
|
|
2804
2885
|
locale: this.isEn(chatId) ? "en" : "zh",
|
|
2805
2886
|
})
|
|
2806
2887
|
: undefined;
|
|
@@ -2900,6 +2981,7 @@ export class FeishuBot {
|
|
|
2900
2981
|
toolCalls: view.toolCalls,
|
|
2901
2982
|
elapsed: view.elapsed,
|
|
2902
2983
|
model: view.model,
|
|
2984
|
+
thinkingLevel: view.thinkingLevel,
|
|
2903
2985
|
locale: en ? "en" : "zh",
|
|
2904
2986
|
}, statusEmoji);
|
|
2905
2987
|
return {
|
|
@@ -2935,8 +3017,12 @@ export class FeishuBot {
|
|
|
2935
3017
|
// Keep the active model visible while the run is in progress or failed.
|
|
2936
3018
|
// The final reply also keeps model attribution so it remains self-contained.
|
|
2937
3019
|
const footerBits = [en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`];
|
|
2938
|
-
if (view.model?.trim())
|
|
2939
|
-
|
|
3020
|
+
if (view.model?.trim()) {
|
|
3021
|
+
const modelBit = view.thinkingLevel?.trim()
|
|
3022
|
+
? `🧠 ${this.escapeCardText(view.model.trim())} · ${this.escapeCardText(view.thinkingLevel.trim())}`
|
|
3023
|
+
: `🧠 ${this.escapeCardText(view.model.trim())}`;
|
|
3024
|
+
footerBits.push(modelBit);
|
|
3025
|
+
}
|
|
2940
3026
|
elements.push({ tag: "markdown", content: `<font color='grey'>${footerBits.join(" · ")}</font>` });
|
|
2941
3027
|
const template = view.state === "failed" ? "orange" : "blue";
|
|
2942
3028
|
return {
|
|
@@ -3023,7 +3109,9 @@ export class FeishuBot {
|
|
|
3023
3109
|
// Running: phase line + ticking elapsed footer.
|
|
3024
3110
|
const phaseLine = view.phase === "transcript-trim"
|
|
3025
3111
|
? (en ? "🧹 Semantic compaction unavailable — trimming transcript safely…" : "🧹 语义压缩不可用,正在安全裁剪转录…")
|
|
3026
|
-
:
|
|
3112
|
+
: view.phase === "trim"
|
|
3113
|
+
? (en ? "🧹 Trimming transcript tail (keeping the most recent events)…" : "🧹 正在裁剪转录尾部(保留最近的事件)…")
|
|
3114
|
+
: (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
|
|
3027
3115
|
const footer = en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`;
|
|
3028
3116
|
return {
|
|
3029
3117
|
schema: "2.0",
|
|
@@ -3117,6 +3205,16 @@ export class FeishuBot {
|
|
|
3117
3205
|
const model = this.replyModelFooters.get(messageId);
|
|
3118
3206
|
const status = this.replyStatusFooters.get(messageId);
|
|
3119
3207
|
const card = this.buildMarkdownCard(text, model, status);
|
|
3208
|
+
// Best-effort chat resolution for chunked/error paths: the reply target is
|
|
3209
|
+
// normally a stored inbound message, so the store knows its chat.
|
|
3210
|
+
const resolveChatId = () => {
|
|
3211
|
+
try {
|
|
3212
|
+
return this.store.getMessageByMessageId(messageId)?.chatId || undefined;
|
|
3213
|
+
}
|
|
3214
|
+
catch {
|
|
3215
|
+
return undefined;
|
|
3216
|
+
}
|
|
3217
|
+
};
|
|
3120
3218
|
try {
|
|
3121
3219
|
const res = await this.client.im.message.reply({
|
|
3122
3220
|
path: { message_id: messageId },
|
|
@@ -3127,17 +3225,35 @@ export class FeishuBot {
|
|
|
3127
3225
|
});
|
|
3128
3226
|
return res?.data?.message_id || res?.message_id;
|
|
3129
3227
|
}
|
|
3130
|
-
catch {
|
|
3228
|
+
catch (err) {
|
|
3229
|
+
const chatId = resolveChatId();
|
|
3230
|
+
if (this.isOutOfChatError(err) && chatId)
|
|
3231
|
+
this.markCurrentBotUnavailable(chatId, err);
|
|
3232
|
+
// Content over Feishu's per-message limit: chunk and deliver instead of
|
|
3233
|
+
// retrying the same oversized payload (retries would fail forever).
|
|
3234
|
+
if (this.isContentTooLongError(err) && chatId) {
|
|
3235
|
+
return await this.sendChunkedText(chatId, text, model, status, messageId);
|
|
3236
|
+
}
|
|
3131
3237
|
// Fallback to plain text if card fails; retain model attribution.
|
|
3132
3238
|
const fallbackText = model?.trim() ? `${text}\n\n🧠 ${model.trim()}${status ? ` · ${status}` : ""}` : text;
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3239
|
+
try {
|
|
3240
|
+
const res = await this.client.im.message.reply({
|
|
3241
|
+
path: { message_id: messageId },
|
|
3242
|
+
data: {
|
|
3243
|
+
content: JSON.stringify({ text: fallbackText }),
|
|
3244
|
+
msg_type: "text",
|
|
3245
|
+
},
|
|
3246
|
+
});
|
|
3247
|
+
return res?.data?.message_id || res?.message_id;
|
|
3248
|
+
}
|
|
3249
|
+
catch (fallbackErr) {
|
|
3250
|
+
if (this.isOutOfChatError(fallbackErr) && chatId)
|
|
3251
|
+
this.markCurrentBotUnavailable(chatId, fallbackErr);
|
|
3252
|
+
if (this.isContentTooLongError(fallbackErr) && chatId) {
|
|
3253
|
+
return await this.sendChunkedText(chatId, text, model, status, messageId);
|
|
3254
|
+
}
|
|
3255
|
+
throw fallbackErr;
|
|
3256
|
+
}
|
|
3141
3257
|
}
|
|
3142
3258
|
}
|
|
3143
3259
|
extractBridgeAttachments(reply) {
|
|
@@ -3485,6 +3601,59 @@ export class FeishuBot {
|
|
|
3485
3601
|
this.sendStatusFooters.delete(chatId);
|
|
3486
3602
|
}
|
|
3487
3603
|
}
|
|
3604
|
+
/** Feishu code 230025: message content over the per-message limit. */
|
|
3605
|
+
isContentTooLongError(err) {
|
|
3606
|
+
const e = err;
|
|
3607
|
+
const code = e?.response?.data?.code ?? e?.data?.code ?? e?.code;
|
|
3608
|
+
const msg = String(e?.response?.data?.msg ?? e?.data?.msg ?? e?.message ?? e?.msg ?? "");
|
|
3609
|
+
return code === 230025 || msg.includes("230025") || /length of the message content/i.test(msg);
|
|
3610
|
+
}
|
|
3611
|
+
/** Split text into Feishu-safe chunks (default well under the 4000-char text limit).
|
|
3612
|
+
* Prefers paragraph, then line boundaries; hard-cuts only when a single line
|
|
3613
|
+
* is longer than the limit. Joining all chunks reproduces the original text. */
|
|
3614
|
+
splitIntoChunks(text, limit = 3200) {
|
|
3615
|
+
if (text.length <= limit)
|
|
3616
|
+
return [text];
|
|
3617
|
+
const chunks = [];
|
|
3618
|
+
let rest = text;
|
|
3619
|
+
while (rest.length > limit) {
|
|
3620
|
+
let cut = rest.lastIndexOf("\n\n", limit);
|
|
3621
|
+
if (cut < limit * 0.5)
|
|
3622
|
+
cut = rest.lastIndexOf("\n", limit);
|
|
3623
|
+
if (cut < limit * 0.5)
|
|
3624
|
+
cut = rest.lastIndexOf(" ", limit);
|
|
3625
|
+
if (cut < limit * 0.5)
|
|
3626
|
+
cut = limit;
|
|
3627
|
+
chunks.push(rest.slice(0, cut));
|
|
3628
|
+
rest = rest.slice(cut).replace(/^\n+/, "");
|
|
3629
|
+
}
|
|
3630
|
+
if (rest.length > 0)
|
|
3631
|
+
chunks.push(rest);
|
|
3632
|
+
return chunks;
|
|
3633
|
+
}
|
|
3634
|
+
/** Deliver oversized content as multiple plain-text messages (used on 230025).
|
|
3635
|
+
* Keeps the model footer on the final chunk. Returns the first message id. */
|
|
3636
|
+
async sendChunkedText(chatId, text, model, status, replyTo) {
|
|
3637
|
+
const suffix = model?.trim() ? `\n\n🧠 ${model.trim()}${status ? ` · ${status}` : ""}` : "";
|
|
3638
|
+
const chunks = this.splitIntoChunks(text);
|
|
3639
|
+
if (suffix && chunks.length > 0)
|
|
3640
|
+
chunks[chunks.length - 1] += suffix;
|
|
3641
|
+
let firstId;
|
|
3642
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
3643
|
+
const payload = {
|
|
3644
|
+
content: JSON.stringify({ text: chunks[i] }),
|
|
3645
|
+
msg_type: "text",
|
|
3646
|
+
};
|
|
3647
|
+
const res = replyTo
|
|
3648
|
+
? await this.client.im.message.reply({ path: { message_id: replyTo }, data: payload })
|
|
3649
|
+
: await this.client.im.message.create({ params: { receive_id_type: "chat_id" }, data: { receive_id: chatId, ...payload } });
|
|
3650
|
+
const id = res?.data?.message_id || res?.message_id;
|
|
3651
|
+
if (i === 0)
|
|
3652
|
+
firstId = id;
|
|
3653
|
+
}
|
|
3654
|
+
this.store.clearBotUnavailableInChat(this.config.name, chatId);
|
|
3655
|
+
return firstId;
|
|
3656
|
+
}
|
|
3488
3657
|
async sendMessage(chatId, text) {
|
|
3489
3658
|
const model = this.sendModelFooters.get(chatId);
|
|
3490
3659
|
const status = this.sendStatusFooters.get(chatId);
|
|
@@ -3505,6 +3674,17 @@ export class FeishuBot {
|
|
|
3505
3674
|
console.warn(`[${this.config.name}] sendMessage interactive failed:`, JSON.stringify(err?.response?.data || err?.data || { message: err.message }));
|
|
3506
3675
|
if (this.isOutOfChatError(err))
|
|
3507
3676
|
this.markCurrentBotUnavailable(chatId, err);
|
|
3677
|
+
// Content over Feishu's per-message limit: chunk and deliver instead of
|
|
3678
|
+
// retrying the same oversized payload (retries would fail forever).
|
|
3679
|
+
if (this.isContentTooLongError(err)) {
|
|
3680
|
+
try {
|
|
3681
|
+
return await this.sendChunkedText(chatId, text, model, status);
|
|
3682
|
+
}
|
|
3683
|
+
catch (chunkErr) {
|
|
3684
|
+
console.warn(`[${this.config.name}] sendMessage chunked delivery failed:`, chunkErr.message);
|
|
3685
|
+
throw chunkErr;
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3508
3688
|
// Fallback to plain text; retain model attribution.
|
|
3509
3689
|
try {
|
|
3510
3690
|
const fallbackText = model?.trim() ? `${text}\n\n🧠 ${model.trim()}${status ? ` · ${status}` : ""}` : text;
|
|
@@ -3523,6 +3703,9 @@ export class FeishuBot {
|
|
|
3523
3703
|
console.warn(`[${this.config.name}] sendMessage text failed:`, JSON.stringify(fallbackErr?.response?.data || fallbackErr?.data || { message: fallbackErr.message }));
|
|
3524
3704
|
if (this.isOutOfChatError(fallbackErr))
|
|
3525
3705
|
this.markCurrentBotUnavailable(chatId, fallbackErr);
|
|
3706
|
+
if (this.isContentTooLongError(fallbackErr)) {
|
|
3707
|
+
return await this.sendChunkedText(chatId, text, model, status);
|
|
3708
|
+
}
|
|
3526
3709
|
throw fallbackErr;
|
|
3527
3710
|
}
|
|
3528
3711
|
}
|
|
@@ -3800,16 +3983,37 @@ export class FeishuBot {
|
|
|
3800
3983
|
* Handle /compact command: compress session context.
|
|
3801
3984
|
*/
|
|
3802
3985
|
/**
|
|
3803
|
-
* Compact a session
|
|
3804
|
-
*
|
|
3805
|
-
* 2. If summarization cannot run, ask the same RPC to retain only a bounded
|
|
3806
|
-
* transcript tail via `maxLines`. This works with legacy JSONL and the
|
|
3807
|
-
* SQLite session store introduced in OpenClaw 2026.8.x, and preserves
|
|
3808
|
-
* OpenClaw's write locks, transcript invariants, and migration ownership.
|
|
3986
|
+
* Compact a session via Gateway-owned RPCs. LMA deliberately never opens or
|
|
3987
|
+
* rewrites OpenClaw transcript storage.
|
|
3809
3988
|
*
|
|
3810
|
-
*
|
|
3989
|
+
* Modes (kept strictly separate so the flows are never mixed):
|
|
3990
|
+
* - "trim" (default): `sessions.compact` with `maxLines` — permanently keeps
|
|
3991
|
+
* only the last N transcript events. Model-free, fast, never times out on
|
|
3992
|
+
* oversized sessions.
|
|
3993
|
+
* - "semantic": LLM summarization via the same RPC; on failure falls back to
|
|
3994
|
+
* a trim so the session is still reclaimed.
|
|
3811
3995
|
*/
|
|
3812
|
-
async compactWithFallback(sessionKey, onPhase) {
|
|
3996
|
+
async compactWithFallback(sessionKey, onPhase, opts = {}) {
|
|
3997
|
+
const mode = opts.mode === "semantic" ? "semantic" : "trim";
|
|
3998
|
+
const trimMaxLines = Number.isFinite(opts.maxLines) && opts.maxLines >= 20
|
|
3999
|
+
? Math.floor(opts.maxLines)
|
|
4000
|
+
: compactFallbackMaxLines();
|
|
4001
|
+
if (mode === "trim") {
|
|
4002
|
+
onPhase?.("trim");
|
|
4003
|
+
try {
|
|
4004
|
+
const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines: trimMaxLines });
|
|
4005
|
+
if (trimmed?.compacted === true) {
|
|
4006
|
+
const kept = typeof trimmed?.kept === "number" ? trimmed.kept : trimMaxLines;
|
|
4007
|
+
return { compacted: true, method: "transcript-trim", detail: `kept ${kept} transcript events` };
|
|
4008
|
+
}
|
|
4009
|
+
const trimReason = typeof trimmed?.reason === "string" ? trimmed.reason : undefined;
|
|
4010
|
+
return { compacted: false, method: "none", reason: trimReason || "transcript trim made no change" };
|
|
4011
|
+
}
|
|
4012
|
+
catch (err) {
|
|
4013
|
+
return { compacted: false, method: "none", reason: err.message };
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
// semantic mode: LLM summarize first, trim as fallback.
|
|
3813
4017
|
let nativeReason;
|
|
3814
4018
|
try {
|
|
3815
4019
|
const res = await this.openclawClient.compactSession(sessionKey);
|
|
@@ -3821,11 +4025,10 @@ export class FeishuBot {
|
|
|
3821
4025
|
nativeReason = err.message;
|
|
3822
4026
|
}
|
|
3823
4027
|
onPhase?.("transcript-trim");
|
|
3824
|
-
const maxLines = compactFallbackMaxLines();
|
|
3825
4028
|
try {
|
|
3826
|
-
const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines });
|
|
4029
|
+
const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines: trimMaxLines });
|
|
3827
4030
|
if (trimmed?.compacted === true) {
|
|
3828
|
-
const kept = typeof trimmed?.kept === "number" ? trimmed.kept :
|
|
4031
|
+
const kept = typeof trimmed?.kept === "number" ? trimmed.kept : trimMaxLines;
|
|
3829
4032
|
return {
|
|
3830
4033
|
compacted: true,
|
|
3831
4034
|
method: "transcript-trim",
|
|
@@ -3850,25 +4053,42 @@ export class FeishuBot {
|
|
|
3850
4053
|
};
|
|
3851
4054
|
}
|
|
3852
4055
|
}
|
|
3853
|
-
async handleCompactCommand(chatId, messageId) {
|
|
4056
|
+
async handleCompactCommand(chatId, messageId, args = "") {
|
|
3854
4057
|
const sessionKey = this.getSessionKey(chatId);
|
|
3855
4058
|
const en = this.isEn(chatId);
|
|
3856
|
-
//
|
|
3857
|
-
//
|
|
3858
|
-
//
|
|
3859
|
-
//
|
|
3860
|
-
//
|
|
4059
|
+
// Argument parsing — modes are strictly separated:
|
|
4060
|
+
// /compact → transcript tail trim (default, model-free)
|
|
4061
|
+
// /compact semantic → LLM summarization (trim fallback on failure)
|
|
4062
|
+
// /compact 500 → tail trim keeping the last 500 events
|
|
4063
|
+
// /compact maxLines=500 → same as above
|
|
4064
|
+
const argTokens = args.split(/\s+/).filter(Boolean);
|
|
4065
|
+
let mode = "trim";
|
|
4066
|
+
let maxLines;
|
|
4067
|
+
for (const tok of argTokens) {
|
|
4068
|
+
const semanticMatch = /^(semantic|summarize|llm)$/i.exec(tok);
|
|
4069
|
+
if (semanticMatch) {
|
|
4070
|
+
mode = "semantic";
|
|
4071
|
+
continue;
|
|
4072
|
+
}
|
|
4073
|
+
const linesMatch = /^(?:maxlines|max_lines)[=:](\d{2,})$/i.exec(tok) || /^(\d{2,})$/.exec(tok);
|
|
4074
|
+
if (linesMatch) {
|
|
4075
|
+
maxLines = Number(linesMatch[1] || linesMatch[2]);
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
3861
4079
|
const progress = new CompactProgressController({
|
|
3862
4080
|
create: (view) => this.sendOrdered(chatId, () => this.replyCompactCard(messageId, view, chatId)),
|
|
3863
4081
|
edit: (id, view) => this.sendOrdered(chatId, () => this.patchCompactCard(id, view, chatId)),
|
|
3864
4082
|
warn: (msg, err) => console.warn(`[${this.config.name}] ${msg}`, err instanceof Error ? err.message : err),
|
|
3865
4083
|
}, { locale: en ? "en" : "zh" });
|
|
4084
|
+
if (mode === "trim")
|
|
4085
|
+
progress.toTrim();
|
|
3866
4086
|
progress.start();
|
|
3867
4087
|
try {
|
|
3868
4088
|
const r = await this.compactWithFallback(sessionKey, (phase) => {
|
|
3869
4089
|
if (phase === "transcript-trim")
|
|
3870
4090
|
void progress.toTranscriptTrim().catch(() => { });
|
|
3871
|
-
});
|
|
4091
|
+
}, { mode, maxLines });
|
|
3872
4092
|
if (r.compacted) {
|
|
3873
4093
|
// If a card was shown, patch it in place; otherwise (fast finish, no card)
|
|
3874
4094
|
// fall back to a normal reply so the user still gets confirmation.
|
|
@@ -4031,9 +4251,6 @@ export class FeishuBot {
|
|
|
4031
4251
|
console.warn(`[${this.config.name}] Failed to fetch chat info:`, err.message);
|
|
4032
4252
|
}
|
|
4033
4253
|
}
|
|
4034
|
-
/**
|
|
4035
|
-
* Send a model-drift notification to the affected chat.
|
|
4036
|
-
*/
|
|
4037
4254
|
async hydrateInlineImageKeys(text, messageId) {
|
|
4038
4255
|
const imageKeyPattern = /\[Image: (img_[^\]\n]+)\]/g;
|
|
4039
4256
|
const replacements = [];
|
|
@@ -4261,24 +4478,4 @@ export class FeishuBot {
|
|
|
4261
4478
|
}
|
|
4262
4479
|
return this.cleanMentions(parts.join(" "));
|
|
4263
4480
|
}
|
|
4264
|
-
async notifyModelDrift(chatId, _sessionKey) {
|
|
4265
|
-
try {
|
|
4266
|
-
const chatInfo = this.store.getChatInfo(chatId);
|
|
4267
|
-
const chatLabel = chatInfo?.chatName || chatId.slice(-8);
|
|
4268
|
-
await this.client.im.message.create({
|
|
4269
|
-
params: { receive_id_type: "chat_id" },
|
|
4270
|
-
data: {
|
|
4271
|
-
receive_id: chatId,
|
|
4272
|
-
content: JSON.stringify({
|
|
4273
|
-
text: `⚠️ 模型漂移已自动纠正\n期望: ${this.config.model}\n已恢复`,
|
|
4274
|
-
}),
|
|
4275
|
-
msg_type: "text",
|
|
4276
|
-
},
|
|
4277
|
-
});
|
|
4278
|
-
console.log(`[${this.config.name}] Drift notification sent to ${chatLabel}`);
|
|
4279
|
-
}
|
|
4280
|
-
catch (err) {
|
|
4281
|
-
console.warn(`[${this.config.name}] Failed to notify drift:`, err.message);
|
|
4282
|
-
}
|
|
4283
|
-
}
|
|
4284
4481
|
}
|
package/dist/live-status.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type LiveStatusView = {
|
|
|
16
16
|
elapsed: string;
|
|
17
17
|
/** Footer: model name, e.g. "phgeek-gw/claude-opus-4.8". */
|
|
18
18
|
model?: string;
|
|
19
|
+
/** Footer: current thinking level label, e.g. "high" / "adaptive". */
|
|
20
|
+
thinkingLevel?: string;
|
|
19
21
|
/** Total tool calls in the run (for the compact finished summary). */
|
|
20
22
|
toolCalls: number;
|
|
21
23
|
/** True when finished via noReply(): the model produced no user-visible reply. */
|
|
@@ -29,6 +31,7 @@ export type LiveStatusFinalMeta = {
|
|
|
29
31
|
toolCalls: number;
|
|
30
32
|
elapsed: string;
|
|
31
33
|
model?: string;
|
|
34
|
+
thinkingLevel?: string;
|
|
32
35
|
locale: "zh" | "en";
|
|
33
36
|
noReply?: boolean;
|
|
34
37
|
};
|
|
@@ -46,6 +49,8 @@ export type LiveStatusOptions = {
|
|
|
46
49
|
maxChars?: number;
|
|
47
50
|
/** Model name shown in the footer. */
|
|
48
51
|
model?: string;
|
|
52
|
+
/** Thinking level label shown after the model name in the footer. */
|
|
53
|
+
thinkingLevel?: string;
|
|
49
54
|
/** Number of recent activity lines kept in the content area. */
|
|
50
55
|
historySize?: number;
|
|
51
56
|
/** Footer/auto-refresh cadence in ms. */
|
package/dist/live-status.js
CHANGED
|
@@ -172,6 +172,7 @@ export class LiveStatusController {
|
|
|
172
172
|
toolCalls: this.toolCallCount,
|
|
173
173
|
elapsed: this.formatElapsed(),
|
|
174
174
|
model: this.opts.model,
|
|
175
|
+
thinkingLevel: this.opts.thinkingLevel,
|
|
175
176
|
locale: this.opts.locale === "en" ? "en" : "zh",
|
|
176
177
|
noReply,
|
|
177
178
|
};
|
|
@@ -403,6 +404,7 @@ export class LiveStatusController {
|
|
|
403
404
|
lines,
|
|
404
405
|
elapsed: this.formatElapsed(),
|
|
405
406
|
model: this.opts.model,
|
|
407
|
+
thinkingLevel: this.opts.thinkingLevel,
|
|
406
408
|
toolCalls: this.toolCallCount,
|
|
407
409
|
noReply: this.noReplyResult,
|
|
408
410
|
state: this.state,
|
|
@@ -143,6 +143,14 @@ export declare class OpenClawClient {
|
|
|
143
143
|
model?: string;
|
|
144
144
|
label?: string;
|
|
145
145
|
}): Promise<any>;
|
|
146
|
+
/** True when an RPC error is the Gateway's "session is archived" refusal. */
|
|
147
|
+
isArchivedSessionError(err: unknown): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Restore an archived session (e.g. auto-archived by the Gateway's
|
|
150
|
+
* active-session-cap) so new work can start. sessions.patch requires
|
|
151
|
+
* expectedSessionId, so describe first, then patch archived=false.
|
|
152
|
+
*/
|
|
153
|
+
unarchiveSession(key: string): Promise<boolean>;
|
|
146
154
|
getSessionStatus(key: string): Promise<any>;
|
|
147
155
|
injectAssistantMessage(params: {
|
|
148
156
|
sessionKey: string;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -1545,6 +1545,34 @@ export class OpenClawClient {
|
|
|
1545
1545
|
async patchSession(params) {
|
|
1546
1546
|
return this.rpc("sessions.patch", { ...params, key: this.canonicalSessionKey(params.key) }, 10000);
|
|
1547
1547
|
}
|
|
1548
|
+
/** True when an RPC error is the Gateway's "session is archived" refusal. */
|
|
1549
|
+
isArchivedSessionError(err) {
|
|
1550
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
1551
|
+
return /is archived[.,]|Restore it before starting new work/i.test(msg);
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Restore an archived session (e.g. auto-archived by the Gateway's
|
|
1555
|
+
* active-session-cap) so new work can start. sessions.patch requires
|
|
1556
|
+
* expectedSessionId, so describe first, then patch archived=false.
|
|
1557
|
+
*/
|
|
1558
|
+
async unarchiveSession(key) {
|
|
1559
|
+
const canonical = this.canonicalSessionKey(key);
|
|
1560
|
+
try {
|
|
1561
|
+
const info = await this.rpc("sessions.describe", { key: canonical }, 15000);
|
|
1562
|
+
const sessionId = info?.session?.sessionId;
|
|
1563
|
+
if (!sessionId)
|
|
1564
|
+
return false;
|
|
1565
|
+
if (info?.session?.archived !== true)
|
|
1566
|
+
return true; // already active
|
|
1567
|
+
const res = await this.rpc("sessions.patch", { key: canonical, expectedSessionId: sessionId, archived: false }, 15000);
|
|
1568
|
+
console.log(`[OpenClaw] unarchived session ${key}: ${res?.ok === true ? "ok" : "unexpected response"}`);
|
|
1569
|
+
return res?.ok === true;
|
|
1570
|
+
}
|
|
1571
|
+
catch (err) {
|
|
1572
|
+
console.warn(`[OpenClaw] unarchive failed for ${key}:`, err instanceof Error ? err.message : err);
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1548
1576
|
async getSessionStatus(key) {
|
|
1549
1577
|
return this.rpc("sessions.describe", { key: this.canonicalSessionKey(key) });
|
|
1550
1578
|
}
|
|
@@ -1801,8 +1829,20 @@ export class OpenClawClient {
|
|
|
1801
1829
|
const maxLines = Number.isFinite(options.maxLines)
|
|
1802
1830
|
? Math.max(1, Math.floor(options.maxLines))
|
|
1803
1831
|
: undefined;
|
|
1832
|
+
const sendCompact = () => this.rpc("sessions.compact", { key: this.canonicalSessionKey(key), ...(maxLines !== undefined ? { maxLines } : {}) }, 10 * 60 * 1000);
|
|
1804
1833
|
try {
|
|
1805
|
-
|
|
1834
|
+
try {
|
|
1835
|
+
return await sendCompact();
|
|
1836
|
+
}
|
|
1837
|
+
catch (err) {
|
|
1838
|
+
if (!this.isArchivedSessionError(err))
|
|
1839
|
+
throw err;
|
|
1840
|
+
console.warn(`[OpenClaw] sessions.compact hit archived session for ${key}; restoring and retrying once`);
|
|
1841
|
+
const restored = await this.unarchiveSession(key);
|
|
1842
|
+
if (!restored)
|
|
1843
|
+
throw err;
|
|
1844
|
+
return await sendCompact();
|
|
1845
|
+
}
|
|
1806
1846
|
}
|
|
1807
1847
|
finally {
|
|
1808
1848
|
const mode = maxLines === undefined ? "semantic" : `maxLines=${maxLines}`;
|
|
@@ -2052,13 +2092,28 @@ export class OpenClawClient {
|
|
|
2052
2092
|
const sendStartedAt = Date.now();
|
|
2053
2093
|
try {
|
|
2054
2094
|
await params.onSendAttempt?.();
|
|
2055
|
-
|
|
2095
|
+
const sendChat = () => this.rpc("chat.send", {
|
|
2056
2096
|
sessionKey: this.canonicalSessionKey(sk),
|
|
2057
2097
|
message: params.message,
|
|
2058
2098
|
attachments: params.attachments,
|
|
2059
2099
|
deliver: params.deliver ?? false,
|
|
2060
2100
|
idempotencyKey: randomUUID(),
|
|
2061
2101
|
});
|
|
2102
|
+
try {
|
|
2103
|
+
result = await sendChat();
|
|
2104
|
+
}
|
|
2105
|
+
catch (err) {
|
|
2106
|
+
// The Gateway auto-archives sessions past the active-session cap; an
|
|
2107
|
+
// archived session refuses new work. Restore it once and retry — the
|
|
2108
|
+
// user should never have to fix this by hand.
|
|
2109
|
+
if (!this.isArchivedSessionError(err))
|
|
2110
|
+
throw err;
|
|
2111
|
+
console.warn(`[OpenClaw] chat.send hit archived session for ${sk}; restoring and retrying once`);
|
|
2112
|
+
const restored = await this.unarchiveSession(sk);
|
|
2113
|
+
if (!restored)
|
|
2114
|
+
throw err;
|
|
2115
|
+
result = await sendChat();
|
|
2116
|
+
}
|
|
2062
2117
|
}
|
|
2063
2118
|
finally {
|
|
2064
2119
|
releaseChatSendSlot();
|