openclaw-lark-multi-agent 1.7.0 → 1.8.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.
@@ -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" | "trim";
2
+ export type CompactPhase = "native" | "transcript-trim" | "trim" | "tooltrim";
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. */
@@ -52,6 +52,8 @@ export declare class CompactProgressController {
52
52
  start(): void;
53
53
  /** Switch straight into explicit transcript-trim mode (user-requested trim). */
54
54
  toTrim(): void;
55
+ /** Switch into ToolTrim mode (keep text, filter tool payload). */
56
+ toToolTrim(): void;
55
57
  /** Native compaction gave up; switch to Gateway-owned transcript-tail trim. */
56
58
  toTranscriptTrim(): Promise<void>;
57
59
  /** Finished successfully; patch the card to the done state with a detail line. */
@@ -47,10 +47,16 @@ export class CompactProgressController {
47
47
  }
48
48
  /** Switch straight into explicit transcript-trim mode (user-requested trim). */
49
49
  toTrim() {
50
- if (this.finalized || this.phase === "trim" || this.phase === "transcript-trim")
50
+ if (this.finalized || this.phase === "trim" || this.phase === "transcript-trim" || this.phase === "tooltrim")
51
51
  return;
52
52
  this.phase = "trim";
53
53
  }
54
+ /** Switch into ToolTrim mode (keep text, filter tool payload). */
55
+ toToolTrim() {
56
+ if (this.finalized || this.phase === "tooltrim" || this.phase === "transcript-trim")
57
+ return;
58
+ this.phase = "tooltrim";
59
+ }
54
60
  /** Native compaction gave up; switch to Gateway-owned transcript-tail trim. */
55
61
  async toTranscriptTrim() {
56
62
  if (this.finalized || this.phase === "transcript-trim")
@@ -137,6 +137,14 @@ export declare class FeishuBot {
137
137
  /** Fire and clear run-failure observers for a chat (isolated callbacks). */
138
138
  private fireRunFailureObservers;
139
139
  private processQueue;
140
+ /**
141
+ * Force-release a stuck queue owner after the gateway authoritatively
142
+ * reported the session idle (no active run). The zombie owner's deep waits
143
+ * can otherwise hold chat custody forever: completion events routed through
144
+ * the proactive path are invisible to them. Steer-fallback triggers are
145
+ * restored via the failure observers so their input drains normally.
146
+ */
147
+ private forceExpireQueueOwner;
140
148
  private processQueueInner;
141
149
  private shouldHandleBridgeCommand;
142
150
  private shouldRespond;
@@ -886,7 +886,7 @@ export class FeishuBot {
886
886
  `━━━━━━━━━━━━━━━━━━`,
887
887
  `桥接层命令(单斜杠,由 openclaw-lark-multi-agent 本地处理)`,
888
888
  `📊 /status — 查看当前模型、Token 用量、Session 状态`,
889
- `🧹 /compact [N|maxLines=N|semantic] — 裁剪转录尾部(默认,保留最近事件);N 指定保留条数;semantic 走 LLM 总结`,
889
+ `🧹 /compact — 默认 ToolTrim:保文本、过滤工具载荷(保留最近 10 条);/compact semantic 走 LLM 总结;/compact trim 或 N 裁剪转录尾部`,
890
890
  `🔄 /reset — 重置当前 bot 的 OpenClaw session`,
891
891
  `⏹️ /stop — 强制停止当前 bot 在本聊天的卡死 run,解锁队列`,
892
892
  `🔊 /verbose — 开关当前聊天里的 Tool Call 显示`,
@@ -1286,7 +1286,39 @@ export class FeishuBot {
1286
1286
  stopWatchingConsumption();
1287
1287
  if ("cancelPending" in steer)
1288
1288
  steer.cancelPending?.();
1289
- console.log(`[${this.config.name}] Agent busy for ${chatId.slice(-8)} (steer=${steer.status}), queuing: "${cleanText.substring(0, 50)}..."`);
1289
+ // Zombie-owner detection (2026-10-15 guangzi incident): steer found no
1290
+ // active run while our in-memory owner still holds the chat. Ask the
1291
+ // gateway for the authoritative session status: if it is idle, the owner
1292
+ // is a zombie (its completion event was routed through the proactive
1293
+ // path and it will never settle). Force-expire it and process this input
1294
+ // NOW instead of queueing behind a dead promise forever.
1295
+ if (steer.status === "no_active_run" && this.queueRuns.has(chatId)) {
1296
+ const live = await this.openclawClient.getSessionRuntimeStatus(sessionKeyForSteer).catch(() => null);
1297
+ if (live && !live.running) {
1298
+ console.warn(`[${this.config.name}] gateway reports session idle while queue owner exists for ${chatId.slice(-8)} — expiring zombie owner (status=${live.status})`);
1299
+ this.forceExpireQueueOwner(chatId, `gateway idle (steer=${steer.status}, session=${live.status})`);
1300
+ // Fall through to the normal send path below; the durable trigger
1301
+ // for this input is still set, so the fresh processQueue drains it.
1302
+ }
1303
+ else {
1304
+ console.log(`[${this.config.name}] Agent busy for ${chatId.slice(-8)} (steer=${steer.status}, session running=${live?.running}), queuing: "${cleanText.substring(0, 50)}..."`);
1305
+ }
1306
+ }
1307
+ else {
1308
+ console.log(`[${this.config.name}] Agent busy for ${chatId.slice(-8)} (steer=${steer.status}), queuing: "${cleanText.substring(0, 50)}..."`);
1309
+ }
1310
+ if (!this.queueRuns.has(chatId)) {
1311
+ // Owner gone (force-expired or settled during the steer RPC): send now.
1312
+ await this.removeReaction(messageId, "Typing").catch(() => { });
1313
+ await this.addReaction(messageId, "Get").catch(() => { });
1314
+ const acksNow = this.pendingAckMessages.get(chatId) || [];
1315
+ for (const a of acksNow)
1316
+ if (a.messageId === messageId)
1317
+ a.emoji = "Get";
1318
+ this.pendingAckMessages.set(chatId, acksNow);
1319
+ await this.processQueue(chatId);
1320
+ return;
1321
+ }
1290
1322
  // Reconciliation may have finished the old owner during the steer RPC.
1291
1323
  // Ensure this new, still-pending input is drained even when that owner
1292
1324
  // exited through an error path. Already-consumed input is never replayed.
@@ -1437,13 +1469,36 @@ export class FeishuBot {
1437
1469
  return existing;
1438
1470
  }
1439
1471
  const run = this.processQueueInner(chatId).finally(() => {
1440
- this.queueRuns.delete(chatId);
1441
- this.runFailureObservers.delete(chatId);
1442
- this.admittedSteerAcks.delete(chatId);
1472
+ // Ownership token guard: only the CURRENT owner may release the slot. A
1473
+ // zombie owner that settles after forceExpireQueueOwner must not delete
1474
+ // the replacement owner's entry (that would open a concurrent-run window).
1475
+ if (this.queueRuns.get(chatId) === run) {
1476
+ this.queueRuns.delete(chatId);
1477
+ this.runFailureObservers.delete(chatId);
1478
+ this.admittedSteerAcks.delete(chatId);
1479
+ }
1443
1480
  });
1444
1481
  this.queueRuns.set(chatId, run);
1445
1482
  return run;
1446
1483
  }
1484
+ /**
1485
+ * Force-release a stuck queue owner after the gateway authoritatively
1486
+ * reported the session idle (no active run). The zombie owner's deep waits
1487
+ * can otherwise hold chat custody forever: completion events routed through
1488
+ * the proactive path are invisible to them. Steer-fallback triggers are
1489
+ * restored via the failure observers so their input drains normally.
1490
+ */
1491
+ forceExpireQueueOwner(chatId, reason) {
1492
+ const owner = this.queueRuns.get(chatId);
1493
+ if (!owner)
1494
+ return false;
1495
+ console.warn(`[${this.config.name}] force-expiring zombie queue owner for ${chatId.slice(-8)}: ${reason}`);
1496
+ this.queueRuns.delete(chatId);
1497
+ this.fireRunFailureObservers(chatId);
1498
+ this.runFailureObservers.delete(chatId);
1499
+ this.admittedSteerAcks.delete(chatId);
1500
+ return true;
1501
+ }
1447
1502
  async processQueueInner(chatId) {
1448
1503
  while (true) {
1449
1504
  // A fresh batch starts: steered inputs admitted into the PREVIOUS batch
@@ -3422,9 +3477,11 @@ export class FeishuBot {
3422
3477
  // Running: phase line + ticking elapsed footer.
3423
3478
  const phaseLine = view.phase === "transcript-trim"
3424
3479
  ? (en ? "🧹 Semantic compaction unavailable — trimming transcript safely…" : "🧹 语义压缩不可用,正在安全裁剪转录…")
3425
- : view.phase === "trim"
3426
- ? (en ? "🧹 Trimming transcript tail (keeping the most recent events)…" : "🧹 正在裁剪转录尾部(保留最近的事件)…")
3427
- : (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
3480
+ : view.phase === "tooltrim"
3481
+ ? (en ? "🧹 ToolTrim: keeping text, filtering tool payload (last 10 tool calls kept)…" : "🧹 ToolTrim 压缩:保留文本,过滤工具载荷(保留最近 10 条工具调用)…")
3482
+ : view.phase === "trim"
3483
+ ? (en ? "🧹 Trimming transcript tail (keeping the most recent events)…" : "🧹 正在裁剪转录尾部(保留最近的事件)…")
3484
+ : (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
3428
3485
  const footer = en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`;
3429
3486
  return {
3430
3487
  schema: "2.0",
@@ -4319,7 +4376,25 @@ export class FeishuBot {
4319
4376
  * a trim so the session is still reclaimed.
4320
4377
  */
4321
4378
  async compactWithFallback(sessionKey, onPhase, opts = {}) {
4322
- const mode = opts.mode === "semantic" ? "semantic" : "trim";
4379
+ const mode = opts.mode === "trim" ? "trim" : opts.mode === "semantic" ? "semantic" : "tooltrim";
4380
+ if (mode === "tooltrim") {
4381
+ onPhase?.("tooltrim");
4382
+ try {
4383
+ const res = await this.openclawClient.toolTrimCompact(sessionKey, 10);
4384
+ if (res?.compacted === true) {
4385
+ return {
4386
+ compacted: true,
4387
+ method: "tooltrim",
4388
+ detail: typeof res?.detail === "string" ? res.detail : "文本保留;仅保留最近 10 条工具调用",
4389
+ };
4390
+ }
4391
+ const reason = typeof res?.detail === "string" ? res.detail : typeof res?.reason === "string" ? res.reason : undefined;
4392
+ return { compacted: false, method: "none", reason: reason || "tooltrim made no change" };
4393
+ }
4394
+ catch (err) {
4395
+ return { compacted: false, method: "none", reason: err.message };
4396
+ }
4397
+ }
4323
4398
  const trimMaxLines = Number.isFinite(opts.maxLines) && opts.maxLines >= 20
4324
4399
  ? Math.floor(opts.maxLines)
4325
4400
  : compactFallbackMaxLines();
@@ -4341,7 +4416,7 @@ export class FeishuBot {
4341
4416
  // semantic mode: LLM summarize first, trim as fallback.
4342
4417
  let nativeReason;
4343
4418
  try {
4344
- const res = await this.openclawClient.compactSession(sessionKey);
4419
+ const res = await this.openclawClient.semanticCompact(sessionKey);
4345
4420
  if (res?.compacted === true)
4346
4421
  return { compacted: true, method: "native" };
4347
4422
  nativeReason = typeof res?.reason === "string" ? res.reason : undefined;
@@ -4387,7 +4462,10 @@ export class FeishuBot {
4387
4462
  // /compact 500 → tail trim keeping the last 500 events
4388
4463
  // /compact maxLines=500 → same as above
4389
4464
  const argTokens = args.split(/\s+/).filter(Boolean);
4390
- let mode = "trim";
4465
+ // toolTrim is the default (user directive 2026-10-10): bare /compact keeps
4466
+ // text and filters tool payload. Explicit tokens: semantic (LLM summarize),
4467
+ // trim/tail or a bare number N (mechanical tail trim keeping N events).
4468
+ let mode = "tooltrim";
4391
4469
  let maxLines;
4392
4470
  for (const tok of argTokens) {
4393
4471
  const semanticMatch = /^(semantic|summarize|llm)$/i.exec(tok);
@@ -4395,9 +4473,20 @@ export class FeishuBot {
4395
4473
  mode = "semantic";
4396
4474
  continue;
4397
4475
  }
4476
+ const toolTrimMatch = /^(tooltrim|tool_trim|tool-trim)$/i.exec(tok);
4477
+ if (toolTrimMatch) {
4478
+ mode = "tooltrim";
4479
+ continue;
4480
+ }
4481
+ const trimMatch = /^(trim|tail)$/i.exec(tok);
4482
+ if (trimMatch) {
4483
+ mode = "trim";
4484
+ continue;
4485
+ }
4398
4486
  const linesMatch = /^(?:maxlines|max_lines)[=:](\d{2,})$/i.exec(tok) || /^(\d{2,})$/.exec(tok);
4399
4487
  if (linesMatch) {
4400
4488
  maxLines = Number(linesMatch[1] || linesMatch[2]);
4489
+ mode = "trim";
4401
4490
  continue;
4402
4491
  }
4403
4492
  }
@@ -4408,6 +4497,8 @@ export class FeishuBot {
4408
4497
  }, { locale: en ? "en" : "zh" });
4409
4498
  if (mode === "trim")
4410
4499
  progress.toTrim();
4500
+ else if (mode === "tooltrim")
4501
+ progress.toToolTrim();
4411
4502
  progress.start();
4412
4503
  try {
4413
4504
  const r = await this.compactWithFallback(sessionKey, (phase) => {
@@ -4420,16 +4511,22 @@ export class FeishuBot {
4420
4511
  const localizedTrimDetail = r.detail
4421
4512
  ?.replace("kept ", en ? "kept " : "保留最近 ")
4422
4513
  .replace(" transcript events", en ? " transcript events" : " 条转录事件");
4423
- const detail = r.method === "transcript-trim" && localizedTrimDetail
4424
- ? (en ? `transcript trim: ${localizedTrimDetail}` : `转录裁剪:${localizedTrimDetail}`)
4425
- : (en ? "native" : "原生压缩");
4514
+ const detail = r.method === "tooltrim"
4515
+ ? (en ? `tooltrim: ${r.detail || "text kept; tool payload filtered"}` : `ToolTrim:${r.detail || "文本保留,工具载荷已过滤"}`)
4516
+ : r.method === "transcript-trim" && localizedTrimDetail
4517
+ ? (en ? `transcript trim: ${localizedTrimDetail}` : `转录裁剪:${localizedTrimDetail}`)
4518
+ : (en ? "native" : "原生压缩");
4426
4519
  await progress.done(detail);
4427
4520
  if (!progress.id) {
4428
- await this.replyMessage(messageId, r.method === "transcript-trim"
4521
+ await this.replyMessage(messageId, r.method === "tooltrim"
4429
4522
  ? (en
4430
- ? `✅ Session compacted (transcript trim${localizedTrimDetail ? ": " + localizedTrimDetail : ""})`
4431
- : `✅ Session 已压缩(转录裁剪${localizedTrimDetail ? ":" + localizedTrimDetail : ""})`)
4432
- : (en ? `✅ Session compacted` : `✅ Session 已压缩`));
4523
+ ? `✅ Session compacted (tooltrim${r.detail ? `: ${r.detail}` : ""})`
4524
+ : `✅ Session 已压缩(ToolTrim${r.detail ? `:${r.detail}` : ""})`)
4525
+ : r.method === "transcript-trim"
4526
+ ? (en
4527
+ ? `✅ Session compacted (transcript trim${localizedTrimDetail ? ": " + localizedTrimDetail : ""})`
4528
+ : `✅ Session 已压缩(转录裁剪${localizedTrimDetail ? ":" + localizedTrimDetail : ""})`)
4529
+ : (en ? `✅ Session compacted` : `✅ Session 已压缩`));
4433
4530
  }
4434
4531
  }
4435
4532
  else {
@@ -178,7 +178,7 @@ export declare class OpenClawClient {
178
178
  * the ambiguous public admission as a realtime-insertion success signal.
179
179
  */
180
180
  steer(sessionKey: string, text: string, displayText?: string): Promise<{
181
- status: "steered" | "unavailable";
181
+ status: "steered" | "unavailable" | "no_active_run";
182
182
  runId?: string;
183
183
  /** Cancel provisional transcript correlation when LMA falls back to a normal run. */
184
184
  cancelPending?: () => void;
@@ -219,6 +219,16 @@ export declare class OpenClawClient {
219
219
  ensureModel(sessionKey: string, expectedModel: string): Promise<boolean>;
220
220
  deleteSession(key: string, deleteTranscript?: boolean): Promise<any>;
221
221
  resetSession(key: string): Promise<any>;
222
+ /** One-shot ToolTrim compaction: route the native semantic compaction
223
+ * through the lma-core plugin's lma-tooltrim provider (keep text, keep only
224
+ * the 10 most recent tool calls, drop the rest of the tool payload).
225
+ * Orchestration (validated E2E 2026-10-10): config.set {raw, baseHash} →
226
+ * sessions.compact → restore the original config (always). */
227
+ toolTrimCompact(key: string, keepRecentToolCalls?: number): Promise<any>;
228
+ /** Explicit LLM-summarize compaction (/compact semantic). Since lma-tooltrim
229
+ * is the persistent default provider, temporarily unset it so the built-in
230
+ * summarization pipeline runs, then restore the original config (always). */
231
+ semanticCompact(key: string): Promise<any>;
222
232
  compactSession(key: string, options?: {
223
233
  maxLines?: number;
224
234
  }): Promise<any>;
@@ -1628,6 +1628,10 @@ export class OpenClawClient {
1628
1628
  console.warn(`[OpenClaw] lma.steer ${res.status} for ${key.slice(-8)}`);
1629
1629
  }
1630
1630
  cancelPending();
1631
+ // Pass the gateway's authoritative "no active run" verdict through: the
1632
+ // caller uses it to detect a zombie queue owner (2026-10-15 incident).
1633
+ if (res?.status === "no_active_run")
1634
+ return { status: "no_active_run" };
1631
1635
  return { status: "unavailable" };
1632
1636
  }
1633
1637
  return { status: "steered", cancelPending };
@@ -1844,6 +1848,117 @@ export class OpenClawClient {
1844
1848
  // sessions.reset may not return a response; use short timeout
1845
1849
  return this.rpc("sessions.reset", { key: this.canonicalSessionKey(key) }, 5000).catch(() => { });
1846
1850
  }
1851
+ /** One-shot ToolTrim compaction: route the native semantic compaction
1852
+ * through the lma-core plugin's lma-tooltrim provider (keep text, keep only
1853
+ * the 10 most recent tool calls, drop the rest of the tool payload).
1854
+ * Orchestration (validated E2E 2026-10-10): config.set {raw, baseHash} →
1855
+ * sessions.compact → restore the original config (always). */
1856
+ async toolTrimCompact(key, keepRecentToolCalls = 10) {
1857
+ const release = await this.acquireMaintenanceSlot("tooltrim");
1858
+ const startedAt = Date.now();
1859
+ const PROVIDER = "lma-tooltrim";
1860
+ let restoreInfo = null;
1861
+ const restoreProvider = async () => {
1862
+ if (!restoreInfo)
1863
+ return;
1864
+ try {
1865
+ await this.rpc("config.set", { raw: restoreInfo.raw, baseHash: restoreInfo.baseHash }, 30000);
1866
+ console.log(`[OpenClaw] tooltrim: compaction provider config restored`);
1867
+ }
1868
+ catch (err) {
1869
+ console.warn(`[OpenClaw] tooltrim: config restore failed:`, err.message);
1870
+ }
1871
+ };
1872
+ try {
1873
+ // 1. Read the full config + its base hash (optimistic-concurrency token).
1874
+ const full = await this.rpc("config.get", {}, 30000);
1875
+ const hash = full?.hash;
1876
+ const cfg = full?.config ?? full?.parsed;
1877
+ if (typeof hash !== "string" || !cfg || typeof cfg !== "object") {
1878
+ return { compacted: false, reason: "无法读取网关配置(config.get)" };
1879
+ }
1880
+ const compNode0 = cfg?.agents?.defaults?.compaction;
1881
+ if (compNode0?.provider !== PROVIDER) {
1882
+ // Provider is not the persistent default: toggle it on for this run and
1883
+ // restore afterwards (portable path for hosts without the default set).
1884
+ const originalRaw = typeof full?.raw === "string" ? full.raw : JSON.stringify(cfg);
1885
+ const agentsNode = (cfg.agents ??= {});
1886
+ const defaultsNode = (agentsNode.defaults ??= {});
1887
+ const comp = (defaultsNode.compaction ??= {});
1888
+ comp.provider = PROVIDER;
1889
+ const setRes = await this.rpc("config.set", { raw: JSON.stringify(cfg), baseHash: hash }, 30000);
1890
+ if (setRes?.ok !== true || typeof setRes?.hash !== "string") {
1891
+ return { compacted: false, reason: `config.set 失败: ${JSON.stringify(setRes).slice(0, 160)}` };
1892
+ }
1893
+ restoreInfo = { raw: originalRaw, baseHash: setRes.hash };
1894
+ }
1895
+ // 2. Native semantic compaction now routes through our provider.
1896
+ const result = await this.rpc("sessions.compact", { key: this.canonicalSessionKey(key) }, 10 * 60 * 1000);
1897
+ await restoreProvider();
1898
+ const ok = result?.compacted === true;
1899
+ return {
1900
+ compacted: ok,
1901
+ method: "tooltrim",
1902
+ detail: ok
1903
+ ? `文本保留;工具载荷已过滤(保留最近 ${keepRecentToolCalls} 条工具调用)`
1904
+ : (typeof result?.reason === "string" ? result.reason : "网关未执行压缩(会话可能正在运行或不需压缩)"),
1905
+ tokensBefore: result?.result?.tokensBefore,
1906
+ };
1907
+ }
1908
+ catch (err) {
1909
+ await restoreProvider();
1910
+ return { compacted: false, method: "tooltrim", reason: err.message };
1911
+ }
1912
+ finally {
1913
+ console.log(`[OpenClaw] tooltrim finished for ${key} in ${Date.now() - startedAt}ms`);
1914
+ release();
1915
+ }
1916
+ }
1917
+ /** Explicit LLM-summarize compaction (/compact semantic). Since lma-tooltrim
1918
+ * is the persistent default provider, temporarily unset it so the built-in
1919
+ * summarization pipeline runs, then restore the original config (always). */
1920
+ async semanticCompact(key) {
1921
+ const release = await this.acquireMaintenanceSlot("semantic");
1922
+ const PROVIDER = "lma-tooltrim";
1923
+ let restoreInfo = null;
1924
+ const restoreProvider = async () => {
1925
+ if (!restoreInfo)
1926
+ return;
1927
+ try {
1928
+ await this.rpc("config.set", { raw: restoreInfo.raw, baseHash: restoreInfo.baseHash }, 30000);
1929
+ console.log(`[OpenClaw] semantic: compaction provider config restored`);
1930
+ }
1931
+ catch (err) {
1932
+ console.warn(`[OpenClaw] semantic: config restore failed:`, err.message);
1933
+ }
1934
+ };
1935
+ try {
1936
+ const full = await this.rpc("config.get", {}, 30000);
1937
+ const hash = full?.hash;
1938
+ const cfg = full?.config ?? full?.parsed;
1939
+ const comp = cfg && typeof cfg === "object" ? cfg?.agents?.defaults?.compaction : undefined;
1940
+ if (typeof hash === "string" && cfg && typeof cfg === "object" && comp?.provider) {
1941
+ const originalRaw = typeof full?.raw === "string" ? full.raw : JSON.stringify(cfg);
1942
+ delete comp.provider;
1943
+ const setRes = await this.rpc("config.set", { raw: JSON.stringify(cfg), baseHash: hash }, 30000);
1944
+ if (setRes?.ok === true && typeof setRes?.hash === "string") {
1945
+ restoreInfo = { raw: originalRaw, baseHash: setRes.hash };
1946
+ }
1947
+ // If the toggle failed we still compact below: routing through the
1948
+ // default provider is a degraded-but-useful outcome vs not compacting.
1949
+ }
1950
+ const result = await this.rpc("sessions.compact", { key: this.canonicalSessionKey(key) }, 10 * 60 * 1000);
1951
+ await restoreProvider();
1952
+ return result;
1953
+ }
1954
+ catch (err) {
1955
+ await restoreProvider();
1956
+ throw err;
1957
+ }
1958
+ finally {
1959
+ release();
1960
+ }
1961
+ }
1847
1962
  async compactSession(key, options = {}) {
1848
1963
  const release = await this.acquireMaintenanceSlot("sessions.compact");
1849
1964
  const startedAt = Date.now();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
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": {
7
- "build": "tsc && npm run build:steer-plugin && npm run build:go-plugin",
7
+ "build": "tsc && npm run build:steer-plugin && npm run build:go-plugin && npm run build:core-plugin",
8
8
  "build:steer-plugin": "tsc -p plugins/lma-steer/tsconfig.json",
9
9
  "start": "node dist/index.js",
10
10
  "dev": "tsx src/index.ts",
@@ -12,7 +12,8 @@
12
12
  "test:coverage": "vitest run --config tests/normal-flow-offline.config.ts --coverage",
13
13
  "prepack": "npm run build",
14
14
  "prepublishOnly": "npm test && npm pack --dry-run",
15
- "build:go-plugin": "tsc -p plugins/lma-go/tsconfig.json"
15
+ "build:go-plugin": "tsc -p plugins/lma-go/tsconfig.json",
16
+ "build:core-plugin": "tsc -p plugins/lma-core/tsconfig.json"
16
17
  },
17
18
  "dependencies": {
18
19
  "@larksuiteoapi/node-sdk": "^1.73.3",
@@ -74,4 +75,4 @@
74
75
  "publishConfig": {
75
76
  "access": "public"
76
77
  }
77
- }
78
+ }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # Update the consolidated lma-core gateway plugin from the LMA repo source.
3
+ #
4
+ # Usage: bash scripts/update-lma-core.sh (build + deploy + hot reload)
5
+ # bash scripts/update-lma-core.sh --check (verify only, no changes)
6
+ #
7
+ # Requirements: repo at ~/openclaw-lark-multi-agent with the plugin sources
8
+ # under plugins/lma-core/src. The gateway hot-reloads the plugin; no gateway
9
+ # restart is needed for code updates (method names are stable).
10
+ set -euo pipefail
11
+
12
+ REPO="${LMA_REPO:-$HOME/openclaw-lark-multi-agent}"
13
+ EXT_DIR="$HOME/.openclaw/extensions/lma-core"
14
+ PLUGIN_ID="lma-core"
15
+
16
+ say() { printf '[update-lma-core] %s\n' "$*"; }
17
+
18
+ if [[ "${1:-}" == "--check" ]]; then
19
+ say "verifying deployed plugin health..."
20
+ openclaw gateway call health 2>/dev/null | grep -o "\"$PLUGIN_ID\"" >/dev/null \
21
+ && say "OK: $PLUGIN_ID loaded in gateway" || { say "FAIL: $PLUGIN_ID not in loaded list"; exit 1; }
22
+ openclaw gateway call lma.core.capabilities 2>/dev/null | grep -q '"lma-core"' \
23
+ && say "OK: lma.core.capabilities responds" || { say "FAIL: capabilities RPC"; exit 1; }
24
+ say "all checks passed."
25
+ exit 0
26
+ fi
27
+
28
+ cd "$REPO"
29
+ say "building plugins/lma-core..."
30
+ npm run build:core-plugin
31
+
32
+ say "deploying to $EXT_DIR..."
33
+ mkdir -p "$EXT_DIR"
34
+ rm -rf "$EXT_DIR/dist"
35
+ cp -r plugins/lma-core/dist "$EXT_DIR/dist"
36
+ cp plugins/lma-core/openclaw.plugin.json plugins/lma-core/package.json "$EXT_DIR/"
37
+ if [[ -f plugins/lma-core/README.md ]]; then cp plugins/lma-core/README.md "$EXT_DIR/"; fi
38
+
39
+ say "hot-reloading $PLUGIN_ID..."
40
+ openclaw plugins reload "$PLUGIN_ID"
41
+
42
+ say "verifying..."
43
+ sleep 1
44
+ bash "$0" --check
45
+ say "done. (rollback: git -C $REPO checkout <prev> && re-run this script)"