openclaw-lark-multi-agent 1.7.0 → 1.8.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" | "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")
@@ -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 显示`,
@@ -3422,9 +3422,11 @@ export class FeishuBot {
3422
3422
  // Running: phase line + ticking elapsed footer.
3423
3423
  const phaseLine = view.phase === "transcript-trim"
3424
3424
  ? (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…");
3425
+ : view.phase === "tooltrim"
3426
+ ? (en ? "🧹 ToolTrim: keeping text, filtering tool payload (last 10 tool calls kept)…" : "🧹 ToolTrim 压缩:保留文本,过滤工具载荷(保留最近 10 条工具调用)…")
3427
+ : view.phase === "trim"
3428
+ ? (en ? "🧹 Trimming transcript tail (keeping the most recent events)…" : "🧹 正在裁剪转录尾部(保留最近的事件)…")
3429
+ : (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
3428
3430
  const footer = en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`;
3429
3431
  return {
3430
3432
  schema: "2.0",
@@ -4319,7 +4321,25 @@ export class FeishuBot {
4319
4321
  * a trim so the session is still reclaimed.
4320
4322
  */
4321
4323
  async compactWithFallback(sessionKey, onPhase, opts = {}) {
4322
- const mode = opts.mode === "semantic" ? "semantic" : "trim";
4324
+ const mode = opts.mode === "trim" ? "trim" : opts.mode === "semantic" ? "semantic" : "tooltrim";
4325
+ if (mode === "tooltrim") {
4326
+ onPhase?.("tooltrim");
4327
+ try {
4328
+ const res = await this.openclawClient.toolTrimCompact(sessionKey, 10);
4329
+ if (res?.compacted === true) {
4330
+ return {
4331
+ compacted: true,
4332
+ method: "tooltrim",
4333
+ detail: typeof res?.detail === "string" ? res.detail : "文本保留;仅保留最近 10 条工具调用",
4334
+ };
4335
+ }
4336
+ const reason = typeof res?.detail === "string" ? res.detail : typeof res?.reason === "string" ? res.reason : undefined;
4337
+ return { compacted: false, method: "none", reason: reason || "tooltrim made no change" };
4338
+ }
4339
+ catch (err) {
4340
+ return { compacted: false, method: "none", reason: err.message };
4341
+ }
4342
+ }
4323
4343
  const trimMaxLines = Number.isFinite(opts.maxLines) && opts.maxLines >= 20
4324
4344
  ? Math.floor(opts.maxLines)
4325
4345
  : compactFallbackMaxLines();
@@ -4341,7 +4361,7 @@ export class FeishuBot {
4341
4361
  // semantic mode: LLM summarize first, trim as fallback.
4342
4362
  let nativeReason;
4343
4363
  try {
4344
- const res = await this.openclawClient.compactSession(sessionKey);
4364
+ const res = await this.openclawClient.semanticCompact(sessionKey);
4345
4365
  if (res?.compacted === true)
4346
4366
  return { compacted: true, method: "native" };
4347
4367
  nativeReason = typeof res?.reason === "string" ? res.reason : undefined;
@@ -4387,7 +4407,10 @@ export class FeishuBot {
4387
4407
  // /compact 500 → tail trim keeping the last 500 events
4388
4408
  // /compact maxLines=500 → same as above
4389
4409
  const argTokens = args.split(/\s+/).filter(Boolean);
4390
- let mode = "trim";
4410
+ // toolTrim is the default (user directive 2026-10-10): bare /compact keeps
4411
+ // text and filters tool payload. Explicit tokens: semantic (LLM summarize),
4412
+ // trim/tail or a bare number N (mechanical tail trim keeping N events).
4413
+ let mode = "tooltrim";
4391
4414
  let maxLines;
4392
4415
  for (const tok of argTokens) {
4393
4416
  const semanticMatch = /^(semantic|summarize|llm)$/i.exec(tok);
@@ -4395,9 +4418,20 @@ export class FeishuBot {
4395
4418
  mode = "semantic";
4396
4419
  continue;
4397
4420
  }
4421
+ const toolTrimMatch = /^(tooltrim|tool_trim|tool-trim)$/i.exec(tok);
4422
+ if (toolTrimMatch) {
4423
+ mode = "tooltrim";
4424
+ continue;
4425
+ }
4426
+ const trimMatch = /^(trim|tail)$/i.exec(tok);
4427
+ if (trimMatch) {
4428
+ mode = "trim";
4429
+ continue;
4430
+ }
4398
4431
  const linesMatch = /^(?:maxlines|max_lines)[=:](\d{2,})$/i.exec(tok) || /^(\d{2,})$/.exec(tok);
4399
4432
  if (linesMatch) {
4400
4433
  maxLines = Number(linesMatch[1] || linesMatch[2]);
4434
+ mode = "trim";
4401
4435
  continue;
4402
4436
  }
4403
4437
  }
@@ -4408,6 +4442,8 @@ export class FeishuBot {
4408
4442
  }, { locale: en ? "en" : "zh" });
4409
4443
  if (mode === "trim")
4410
4444
  progress.toTrim();
4445
+ else if (mode === "tooltrim")
4446
+ progress.toToolTrim();
4411
4447
  progress.start();
4412
4448
  try {
4413
4449
  const r = await this.compactWithFallback(sessionKey, (phase) => {
@@ -4420,16 +4456,22 @@ export class FeishuBot {
4420
4456
  const localizedTrimDetail = r.detail
4421
4457
  ?.replace("kept ", en ? "kept " : "保留最近 ")
4422
4458
  .replace(" transcript events", en ? " transcript events" : " 条转录事件");
4423
- const detail = r.method === "transcript-trim" && localizedTrimDetail
4424
- ? (en ? `transcript trim: ${localizedTrimDetail}` : `转录裁剪:${localizedTrimDetail}`)
4425
- : (en ? "native" : "原生压缩");
4459
+ const detail = r.method === "tooltrim"
4460
+ ? (en ? `tooltrim: ${r.detail || "text kept; tool payload filtered"}` : `ToolTrim:${r.detail || "文本保留,工具载荷已过滤"}`)
4461
+ : r.method === "transcript-trim" && localizedTrimDetail
4462
+ ? (en ? `transcript trim: ${localizedTrimDetail}` : `转录裁剪:${localizedTrimDetail}`)
4463
+ : (en ? "native" : "原生压缩");
4426
4464
  await progress.done(detail);
4427
4465
  if (!progress.id) {
4428
- await this.replyMessage(messageId, r.method === "transcript-trim"
4466
+ await this.replyMessage(messageId, r.method === "tooltrim"
4429
4467
  ? (en
4430
- ? `✅ Session compacted (transcript trim${localizedTrimDetail ? ": " + localizedTrimDetail : ""})`
4431
- : `✅ Session 已压缩(转录裁剪${localizedTrimDetail ? ":" + localizedTrimDetail : ""})`)
4432
- : (en ? `✅ Session compacted` : `✅ Session 已压缩`));
4468
+ ? `✅ Session compacted (tooltrim${r.detail ? `: ${r.detail}` : ""})`
4469
+ : `✅ Session 已压缩(ToolTrim${r.detail ? `:${r.detail}` : ""})`)
4470
+ : r.method === "transcript-trim"
4471
+ ? (en
4472
+ ? `✅ Session compacted (transcript trim${localizedTrimDetail ? ": " + localizedTrimDetail : ""})`
4473
+ : `✅ Session 已压缩(转录裁剪${localizedTrimDetail ? ":" + localizedTrimDetail : ""})`)
4474
+ : (en ? `✅ Session compacted` : `✅ Session 已压缩`));
4433
4475
  }
4434
4476
  }
4435
4477
  else {
@@ -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>;
@@ -1844,6 +1844,117 @@ export class OpenClawClient {
1844
1844
  // sessions.reset may not return a response; use short timeout
1845
1845
  return this.rpc("sessions.reset", { key: this.canonicalSessionKey(key) }, 5000).catch(() => { });
1846
1846
  }
1847
+ /** One-shot ToolTrim compaction: route the native semantic compaction
1848
+ * through the lma-core plugin's lma-tooltrim provider (keep text, keep only
1849
+ * the 10 most recent tool calls, drop the rest of the tool payload).
1850
+ * Orchestration (validated E2E 2026-10-10): config.set {raw, baseHash} →
1851
+ * sessions.compact → restore the original config (always). */
1852
+ async toolTrimCompact(key, keepRecentToolCalls = 10) {
1853
+ const release = await this.acquireMaintenanceSlot("tooltrim");
1854
+ const startedAt = Date.now();
1855
+ const PROVIDER = "lma-tooltrim";
1856
+ let restoreInfo = null;
1857
+ const restoreProvider = async () => {
1858
+ if (!restoreInfo)
1859
+ return;
1860
+ try {
1861
+ await this.rpc("config.set", { raw: restoreInfo.raw, baseHash: restoreInfo.baseHash }, 30000);
1862
+ console.log(`[OpenClaw] tooltrim: compaction provider config restored`);
1863
+ }
1864
+ catch (err) {
1865
+ console.warn(`[OpenClaw] tooltrim: config restore failed:`, err.message);
1866
+ }
1867
+ };
1868
+ try {
1869
+ // 1. Read the full config + its base hash (optimistic-concurrency token).
1870
+ const full = await this.rpc("config.get", {}, 30000);
1871
+ const hash = full?.hash;
1872
+ const cfg = full?.config ?? full?.parsed;
1873
+ if (typeof hash !== "string" || !cfg || typeof cfg !== "object") {
1874
+ return { compacted: false, reason: "无法读取网关配置(config.get)" };
1875
+ }
1876
+ const compNode0 = cfg?.agents?.defaults?.compaction;
1877
+ if (compNode0?.provider !== PROVIDER) {
1878
+ // Provider is not the persistent default: toggle it on for this run and
1879
+ // restore afterwards (portable path for hosts without the default set).
1880
+ const originalRaw = typeof full?.raw === "string" ? full.raw : JSON.stringify(cfg);
1881
+ const agentsNode = (cfg.agents ??= {});
1882
+ const defaultsNode = (agentsNode.defaults ??= {});
1883
+ const comp = (defaultsNode.compaction ??= {});
1884
+ comp.provider = PROVIDER;
1885
+ const setRes = await this.rpc("config.set", { raw: JSON.stringify(cfg), baseHash: hash }, 30000);
1886
+ if (setRes?.ok !== true || typeof setRes?.hash !== "string") {
1887
+ return { compacted: false, reason: `config.set 失败: ${JSON.stringify(setRes).slice(0, 160)}` };
1888
+ }
1889
+ restoreInfo = { raw: originalRaw, baseHash: setRes.hash };
1890
+ }
1891
+ // 2. Native semantic compaction now routes through our provider.
1892
+ const result = await this.rpc("sessions.compact", { key: this.canonicalSessionKey(key) }, 10 * 60 * 1000);
1893
+ await restoreProvider();
1894
+ const ok = result?.compacted === true;
1895
+ return {
1896
+ compacted: ok,
1897
+ method: "tooltrim",
1898
+ detail: ok
1899
+ ? `文本保留;工具载荷已过滤(保留最近 ${keepRecentToolCalls} 条工具调用)`
1900
+ : (typeof result?.reason === "string" ? result.reason : "网关未执行压缩(会话可能正在运行或不需压缩)"),
1901
+ tokensBefore: result?.result?.tokensBefore,
1902
+ };
1903
+ }
1904
+ catch (err) {
1905
+ await restoreProvider();
1906
+ return { compacted: false, method: "tooltrim", reason: err.message };
1907
+ }
1908
+ finally {
1909
+ console.log(`[OpenClaw] tooltrim finished for ${key} in ${Date.now() - startedAt}ms`);
1910
+ release();
1911
+ }
1912
+ }
1913
+ /** Explicit LLM-summarize compaction (/compact semantic). Since lma-tooltrim
1914
+ * is the persistent default provider, temporarily unset it so the built-in
1915
+ * summarization pipeline runs, then restore the original config (always). */
1916
+ async semanticCompact(key) {
1917
+ const release = await this.acquireMaintenanceSlot("semantic");
1918
+ const PROVIDER = "lma-tooltrim";
1919
+ let restoreInfo = null;
1920
+ const restoreProvider = async () => {
1921
+ if (!restoreInfo)
1922
+ return;
1923
+ try {
1924
+ await this.rpc("config.set", { raw: restoreInfo.raw, baseHash: restoreInfo.baseHash }, 30000);
1925
+ console.log(`[OpenClaw] semantic: compaction provider config restored`);
1926
+ }
1927
+ catch (err) {
1928
+ console.warn(`[OpenClaw] semantic: config restore failed:`, err.message);
1929
+ }
1930
+ };
1931
+ try {
1932
+ const full = await this.rpc("config.get", {}, 30000);
1933
+ const hash = full?.hash;
1934
+ const cfg = full?.config ?? full?.parsed;
1935
+ const comp = cfg && typeof cfg === "object" ? cfg?.agents?.defaults?.compaction : undefined;
1936
+ if (typeof hash === "string" && cfg && typeof cfg === "object" && comp?.provider) {
1937
+ const originalRaw = typeof full?.raw === "string" ? full.raw : JSON.stringify(cfg);
1938
+ delete comp.provider;
1939
+ const setRes = await this.rpc("config.set", { raw: JSON.stringify(cfg), baseHash: hash }, 30000);
1940
+ if (setRes?.ok === true && typeof setRes?.hash === "string") {
1941
+ restoreInfo = { raw: originalRaw, baseHash: setRes.hash };
1942
+ }
1943
+ // If the toggle failed we still compact below: routing through the
1944
+ // default provider is a degraded-but-useful outcome vs not compacting.
1945
+ }
1946
+ const result = await this.rpc("sessions.compact", { key: this.canonicalSessionKey(key) }, 10 * 60 * 1000);
1947
+ await restoreProvider();
1948
+ return result;
1949
+ }
1950
+ catch (err) {
1951
+ await restoreProvider();
1952
+ throw err;
1953
+ }
1954
+ finally {
1955
+ release();
1956
+ }
1957
+ }
1847
1958
  async compactSession(key, options = {}) {
1848
1959
  const release = await this.acquireMaintenanceSlot("sessions.compact");
1849
1960
  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.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": {
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)"