openclaw-lark-multi-agent 1.4.13 → 1.5.0

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.
@@ -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. */
@@ -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
@@ -5,6 +5,8 @@ export interface BotConfig {
5
5
  appSecret: string;
6
6
  model: string;
7
7
  locale?: Locale;
8
+ /** Optional per-bot thinking level (informational fallback for footers). */
9
+ thinking?: string;
8
10
  }
9
11
  export interface OpenClawConfig {
10
12
  baseUrl: string;
@@ -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;
@@ -316,14 +320,15 @@ export declare class FeishuBot {
316
320
  * Handle /compact command: compress session context.
317
321
  */
318
322
  /**
319
- * Compact a session with a Gateway-owned fallback chain:
320
- * 1. Semantic compaction through `sessions.compact`.
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.
323
+ * Compact a session via Gateway-owned RPCs. LMA deliberately never opens or
324
+ * rewrites OpenClaw transcript storage.
325
325
  *
326
- * LMA deliberately never opens or rewrites OpenClaw transcript storage.
326
+ * Modes (kept strictly separate so the flows are never mixed):
327
+ * - "trim" (default): `sessions.compact` with `maxLines` — permanently keeps
328
+ * only the last N transcript events. Model-free, fast, never times out on
329
+ * oversized sessions.
330
+ * - "semantic": LLM summarization via the same RPC; on failure falls back to
331
+ * a trim so the session is still reclaimed.
327
332
  */
328
333
  private compactWithFallback;
329
334
  private handleCompactCommand;
@@ -344,9 +349,6 @@ export declare class FeishuBot {
344
349
  * Called once per chat on first message.
345
350
  */
346
351
  private fetchAndCacheChatInfo;
347
- /**
348
- * Send a model-drift notification to the affected chat.
349
- */
350
352
  private hydrateInlineImageKeys;
351
353
  private hydrateFeishuDocsInMessage;
352
354
  private extractFeishuDocRefs;
@@ -365,5 +367,4 @@ export declare class FeishuBot {
365
367
  * Extract text content from a rich text (post) message.
366
368
  */
367
369
  private extractPostText;
368
- private notifyModelDrift;
369
370
  }
@@ -44,12 +44,12 @@ function steerInjectionPrefix(en) {
44
44
  || "[New message inserted by the user mid-task; please handle it together with the current task]";
45
45
  return en ? enText : zh;
46
46
  }
47
- // Emergency transcript trim used only when semantic compaction cannot run.
48
- // The Gateway owns the transcript store (JSONL before 2026.8, SQLite after it),
49
- // so LMA must use sessions.compact instead of mutating storage directly.
47
+ // Default transcript-tail trim depth for /compact. Tool-heavy group sessions can
48
+ // easily carry 300+ events of pure tool noise, so keep a generous tail: the
49
+ // Gateway truncates to the last N transcript events. Env override stays.
50
50
  function compactFallbackMaxLines() {
51
- const value = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_FALLBACK_MAX_LINES || 200);
52
- return Number.isFinite(value) ? Math.max(20, Math.floor(value)) : 200;
51
+ const value = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_FALLBACK_MAX_LINES || 800);
52
+ return Number.isFinite(value) ? Math.max(20, Math.floor(value)) : 800;
53
53
  }
54
54
  // After a reply is delivered, if context usage reaches this percent, send the
55
55
  // user a one-time alert to compact. 0 disables the alert.
@@ -212,7 +212,6 @@ export class FeishuBot {
212
212
  const corrected = await this.openclawClient.ensureModel(sessionKey, this.config.model);
213
213
  if (corrected) {
214
214
  console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
215
- await this.notifyModelDrift(chatId, sessionKey);
216
215
  }
217
216
  return sessionKey;
218
217
  }
@@ -225,7 +224,6 @@ export class FeishuBot {
225
224
  const corrected = await this.openclawClient.ensureModel(sessionKey, this.config.model);
226
225
  if (corrected) {
227
226
  console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
228
- await this.notifyModelDrift(chatId, sessionKey);
229
227
  }
230
228
  }
231
229
  else {
@@ -683,7 +681,7 @@ export class FeishuBot {
683
681
  `━━━━━━━━━━━━━━━━━━`,
684
682
  `桥接层命令(单斜杠,由 openclaw-lark-multi-agent 本地处理)`,
685
683
  `📊 /status — 查看当前模型、Token 用量、Session 状态`,
686
- `🧹 /compact — 压缩当前 bot 的 OpenClaw session`,
684
+ `🧹 /compact [N|maxLines=N|semantic] — 裁剪转录尾部(默认,保留最近事件);N 指定保留条数;semantic 走 LLM 总结`,
687
685
  `🔄 /reset — 重置当前 bot 的 OpenClaw session`,
688
686
  `⏹️ /stop — 强制停止当前 bot 在本聊天的卡死 run,解锁队列`,
689
687
  `🔊 /verbose — 开关当前聊天里的 Tool Call 显示`,
@@ -729,7 +727,8 @@ export class FeishuBot {
729
727
  }
730
728
  if (commandName === "/compact") {
731
729
  await this.ensureSession(chatId);
732
- await this.handleCompactCommand(chatId, messageId);
730
+ const compactArgs = cleanText.trim().split(/\s+/).slice(1).join(" ").trim();
731
+ await this.handleCompactCommand(chatId, messageId, compactArgs);
733
732
  markCommandSynced();
734
733
  return;
735
734
  }
@@ -1360,6 +1359,7 @@ export class FeishuBot {
1360
1359
  }, {
1361
1360
  botName: this.config.name,
1362
1361
  model: this.config.model,
1362
+ thinkingLevel: await this.fetchThinkingLabel(sessionKey),
1363
1363
  locale: this.isEn(chatId) ? "en" : "zh",
1364
1364
  })
1365
1365
  : undefined;
@@ -1888,7 +1888,12 @@ export class FeishuBot {
1888
1888
  const core = en
1889
1889
  ? `${emoji} ${meta.toolCalls} tool call${meta.toolCalls === 1 ? "" : "s"} · ⏱ ${meta.elapsed}`
1890
1890
  : `${emoji} 累计${meta.toolCalls} 次工具调用 · ⏱ 耗时${meta.elapsed}`;
1891
- return meta.model?.trim() ? `${core} · 🧠 ${this.escapeCardText(meta.model.trim())}` : core;
1891
+ if (!meta.model?.trim())
1892
+ return core;
1893
+ const modelLabel = meta.thinkingLevel?.trim()
1894
+ ? `${meta.model.trim()} · ${meta.thinkingLevel.trim()}`
1895
+ : meta.model.trim();
1896
+ return `${core} · 🧠 ${this.escapeCardText(modelLabel)}`;
1892
1897
  }
1893
1898
  /** Best-effort terminal cleanup for status-only rows and legacy v1.4.3
1894
1899
  * outbox entries that once targeted the live-status card. */
@@ -1898,6 +1903,7 @@ export class FeishuBot {
1898
1903
  lines: [],
1899
1904
  elapsed: meta.elapsed,
1900
1905
  model: meta.model,
1906
+ thinkingLevel: meta.thinkingLevel,
1901
1907
  toolCalls: meta.toolCalls,
1902
1908
  noReply: Boolean(meta.noReply),
1903
1909
  state: "done",
@@ -2783,6 +2789,24 @@ export class FeishuBot {
2783
2789
  const bot = Array.from(FeishuBot.allBots.values()).find((candidate) => candidate.store === this.store && candidate.config.name === chairman && this.isBotAvailableInChat(candidate, chatId));
2784
2790
  return bot ? this.asDiscussionParticipant(bot, chatId) : undefined;
2785
2791
  }
2792
+ /** Resolve the session's current thinking label for the live-status footer.
2793
+ * Prefers a live session override (set via //think / sessions.patch), falls
2794
+ * back to the session's default, then the bot's config-level thinking. */
2795
+ async fetchThinkingLabel(sessionKey) {
2796
+ try {
2797
+ const info = await this.openclawClient.getSessionInfo(sessionKey);
2798
+ const s = info?.session;
2799
+ const label = (typeof s?.thinkingLevel === "string" && s.thinkingLevel.trim())
2800
+ || (typeof s?.thinkingDefault === "string" && s.thinkingDefault.trim())
2801
+ || (typeof this.config.thinking === "string" && this.config.thinking.trim())
2802
+ || "";
2803
+ return label || undefined;
2804
+ }
2805
+ catch {
2806
+ const cfg = typeof this.config.thinking === "string" ? this.config.thinking.trim() : "";
2807
+ return cfg || undefined;
2808
+ }
2809
+ }
2786
2810
  asDiscussionParticipant(bot, chatId) {
2787
2811
  return {
2788
2812
  name: bot.config.name,
@@ -2801,6 +2825,7 @@ export class FeishuBot {
2801
2825
  }, {
2802
2826
  botName: this.config.name,
2803
2827
  model: this.config.model,
2828
+ thinkingLevel: await this.fetchThinkingLabel(sessionKey),
2804
2829
  locale: this.isEn(chatId) ? "en" : "zh",
2805
2830
  })
2806
2831
  : undefined;
@@ -2900,6 +2925,7 @@ export class FeishuBot {
2900
2925
  toolCalls: view.toolCalls,
2901
2926
  elapsed: view.elapsed,
2902
2927
  model: view.model,
2928
+ thinkingLevel: view.thinkingLevel,
2903
2929
  locale: en ? "en" : "zh",
2904
2930
  }, statusEmoji);
2905
2931
  return {
@@ -2935,8 +2961,12 @@ export class FeishuBot {
2935
2961
  // Keep the active model visible while the run is in progress or failed.
2936
2962
  // The final reply also keeps model attribution so it remains self-contained.
2937
2963
  const footerBits = [en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`];
2938
- if (view.model?.trim())
2939
- footerBits.push(`🧠 ${this.escapeCardText(view.model.trim())}`);
2964
+ if (view.model?.trim()) {
2965
+ const modelBit = view.thinkingLevel?.trim()
2966
+ ? `🧠 ${this.escapeCardText(view.model.trim())} · ${this.escapeCardText(view.thinkingLevel.trim())}`
2967
+ : `🧠 ${this.escapeCardText(view.model.trim())}`;
2968
+ footerBits.push(modelBit);
2969
+ }
2940
2970
  elements.push({ tag: "markdown", content: `<font color='grey'>${footerBits.join(" · ")}</font>` });
2941
2971
  const template = view.state === "failed" ? "orange" : "blue";
2942
2972
  return {
@@ -3023,7 +3053,9 @@ export class FeishuBot {
3023
3053
  // Running: phase line + ticking elapsed footer.
3024
3054
  const phaseLine = view.phase === "transcript-trim"
3025
3055
  ? (en ? "🧹 Semantic compaction unavailable — trimming transcript safely…" : "🧹 语义压缩不可用,正在安全裁剪转录…")
3026
- : (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
3056
+ : view.phase === "trim"
3057
+ ? (en ? "🧹 Trimming transcript tail (keeping the most recent events)…" : "🧹 正在裁剪转录尾部(保留最近的事件)…")
3058
+ : (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
3027
3059
  const footer = en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`;
3028
3060
  return {
3029
3061
  schema: "2.0",
@@ -3800,16 +3832,37 @@ export class FeishuBot {
3800
3832
  * Handle /compact command: compress session context.
3801
3833
  */
3802
3834
  /**
3803
- * Compact a session with a Gateway-owned fallback chain:
3804
- * 1. Semantic compaction through `sessions.compact`.
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.
3835
+ * Compact a session via Gateway-owned RPCs. LMA deliberately never opens or
3836
+ * rewrites OpenClaw transcript storage.
3809
3837
  *
3810
- * LMA deliberately never opens or rewrites OpenClaw transcript storage.
3838
+ * Modes (kept strictly separate so the flows are never mixed):
3839
+ * - "trim" (default): `sessions.compact` with `maxLines` — permanently keeps
3840
+ * only the last N transcript events. Model-free, fast, never times out on
3841
+ * oversized sessions.
3842
+ * - "semantic": LLM summarization via the same RPC; on failure falls back to
3843
+ * a trim so the session is still reclaimed.
3811
3844
  */
3812
- async compactWithFallback(sessionKey, onPhase) {
3845
+ async compactWithFallback(sessionKey, onPhase, opts = {}) {
3846
+ const mode = opts.mode === "semantic" ? "semantic" : "trim";
3847
+ const trimMaxLines = Number.isFinite(opts.maxLines) && opts.maxLines >= 20
3848
+ ? Math.floor(opts.maxLines)
3849
+ : compactFallbackMaxLines();
3850
+ if (mode === "trim") {
3851
+ onPhase?.("trim");
3852
+ try {
3853
+ const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines: trimMaxLines });
3854
+ if (trimmed?.compacted === true) {
3855
+ const kept = typeof trimmed?.kept === "number" ? trimmed.kept : trimMaxLines;
3856
+ return { compacted: true, method: "transcript-trim", detail: `kept ${kept} transcript events` };
3857
+ }
3858
+ const trimReason = typeof trimmed?.reason === "string" ? trimmed.reason : undefined;
3859
+ return { compacted: false, method: "none", reason: trimReason || "transcript trim made no change" };
3860
+ }
3861
+ catch (err) {
3862
+ return { compacted: false, method: "none", reason: err.message };
3863
+ }
3864
+ }
3865
+ // semantic mode: LLM summarize first, trim as fallback.
3813
3866
  let nativeReason;
3814
3867
  try {
3815
3868
  const res = await this.openclawClient.compactSession(sessionKey);
@@ -3821,11 +3874,10 @@ export class FeishuBot {
3821
3874
  nativeReason = err.message;
3822
3875
  }
3823
3876
  onPhase?.("transcript-trim");
3824
- const maxLines = compactFallbackMaxLines();
3825
3877
  try {
3826
- const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines });
3878
+ const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines: trimMaxLines });
3827
3879
  if (trimmed?.compacted === true) {
3828
- const kept = typeof trimmed?.kept === "number" ? trimmed.kept : maxLines;
3880
+ const kept = typeof trimmed?.kept === "number" ? trimmed.kept : trimMaxLines;
3829
3881
  return {
3830
3882
  compacted: true,
3831
3883
  method: "transcript-trim",
@@ -3850,25 +3902,42 @@ export class FeishuBot {
3850
3902
  };
3851
3903
  }
3852
3904
  }
3853
- async handleCompactCommand(chatId, messageId) {
3905
+ async handleCompactCommand(chatId, messageId, args = "") {
3854
3906
  const sessionKey = this.getSessionKey(chatId);
3855
3907
  const en = this.isEn(chatId);
3856
- // Large sessions can take tens of seconds to compact (native compaction may
3857
- // even time out before the transcript-trim fallback kicks in). Show a ticking
3858
- // "compacting…" card so the user can SEE it is working instead of staring at
3859
- // nothing and assuming nothing is happening. The card is created lazily, so
3860
- // a fast compaction that finishes quickly never flashes a card at all.
3908
+ // Argument parsing — modes are strictly separated:
3909
+ // /compact → transcript tail trim (default, model-free)
3910
+ // /compact semantic → LLM summarization (trim fallback on failure)
3911
+ // /compact 500 → tail trim keeping the last 500 events
3912
+ // /compact maxLines=500 → same as above
3913
+ const argTokens = args.split(/\s+/).filter(Boolean);
3914
+ let mode = "trim";
3915
+ let maxLines;
3916
+ for (const tok of argTokens) {
3917
+ const semanticMatch = /^(semantic|summarize|llm)$/i.exec(tok);
3918
+ if (semanticMatch) {
3919
+ mode = "semantic";
3920
+ continue;
3921
+ }
3922
+ const linesMatch = /^(?:maxlines|max_lines)[=:](\d{2,})$/i.exec(tok) || /^(\d{2,})$/.exec(tok);
3923
+ if (linesMatch) {
3924
+ maxLines = Number(linesMatch[1] || linesMatch[2]);
3925
+ continue;
3926
+ }
3927
+ }
3861
3928
  const progress = new CompactProgressController({
3862
3929
  create: (view) => this.sendOrdered(chatId, () => this.replyCompactCard(messageId, view, chatId)),
3863
3930
  edit: (id, view) => this.sendOrdered(chatId, () => this.patchCompactCard(id, view, chatId)),
3864
3931
  warn: (msg, err) => console.warn(`[${this.config.name}] ${msg}`, err instanceof Error ? err.message : err),
3865
3932
  }, { locale: en ? "en" : "zh" });
3933
+ if (mode === "trim")
3934
+ progress.toTrim();
3866
3935
  progress.start();
3867
3936
  try {
3868
3937
  const r = await this.compactWithFallback(sessionKey, (phase) => {
3869
3938
  if (phase === "transcript-trim")
3870
3939
  void progress.toTranscriptTrim().catch(() => { });
3871
- });
3940
+ }, { mode, maxLines });
3872
3941
  if (r.compacted) {
3873
3942
  // If a card was shown, patch it in place; otherwise (fast finish, no card)
3874
3943
  // fall back to a normal reply so the user still gets confirmation.
@@ -4031,9 +4100,6 @@ export class FeishuBot {
4031
4100
  console.warn(`[${this.config.name}] Failed to fetch chat info:`, err.message);
4032
4101
  }
4033
4102
  }
4034
- /**
4035
- * Send a model-drift notification to the affected chat.
4036
- */
4037
4103
  async hydrateInlineImageKeys(text, messageId) {
4038
4104
  const imageKeyPattern = /\[Image: (img_[^\]\n]+)\]/g;
4039
4105
  const replacements = [];
@@ -4261,24 +4327,4 @@ export class FeishuBot {
4261
4327
  }
4262
4328
  return this.cleanMentions(parts.join(" "));
4263
4329
  }
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
4330
  }
@@ -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. */
@@ -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;
@@ -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
- return await this.rpc("sessions.compact", { key: this.canonicalSessionKey(key), ...(maxLines !== undefined ? { maxLines } : {}) }, 10 * 60 * 1000);
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
- result = await this.rpc("chat.send", {
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.4.13",
3
+ "version": "1.5.0",
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": {