openclaw-lark-multi-agent 1.4.4 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -15
- package/README.zh-CN.md +8 -0
- package/dist/cli.js +4 -4
- package/dist/compact-progress.d.ts +4 -4
- package/dist/compact-progress.js +5 -5
- package/dist/feishu-bot.d.ts +19 -9
- package/dist/feishu-bot.js +214 -94
- package/dist/openclaw-client.d.ts +32 -25
- package/dist/openclaw-client.js +327 -158
- package/package.json +2 -2
- package/plugins/lma-steer/README.md +23 -30
- package/dist/session-file-compactor.d.ts +0 -39
- package/dist/session-file-compactor.js +0 -176
package/dist/feishu-bot.js
CHANGED
|
@@ -9,7 +9,6 @@ import { basename, extname, join, resolve } from "path";
|
|
|
9
9
|
import { getBridgeAttachmentsDir, getDataDir } from "./paths.js";
|
|
10
10
|
import { buildFeishuCardElements } from "./markdown.js";
|
|
11
11
|
import { discussionManager } from "./discussion-manager.js";
|
|
12
|
-
import { resolveSessionFilePath, toolTrimCompactFile } from "./session-file-compactor.js";
|
|
13
12
|
const require = createRequire(import.meta.url);
|
|
14
13
|
const LMA_VERSION = require("../package.json").version;
|
|
15
14
|
const MAX_BOT_STREAK = 10;
|
|
@@ -42,9 +41,13 @@ function steerInjectionPrefix(en) {
|
|
|
42
41
|
|| "[New message inserted by the user mid-task; please handle it together with the current task]";
|
|
43
42
|
return en ? enText : zh;
|
|
44
43
|
}
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
44
|
+
// Emergency transcript trim used only when semantic compaction cannot run.
|
|
45
|
+
// The Gateway owns the transcript store (JSONL before 2026.8, SQLite after it),
|
|
46
|
+
// so LMA must use sessions.compact instead of mutating storage directly.
|
|
47
|
+
function compactFallbackMaxLines() {
|
|
48
|
+
const value = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_FALLBACK_MAX_LINES || 200);
|
|
49
|
+
return Number.isFinite(value) ? Math.max(20, Math.floor(value)) : 200;
|
|
50
|
+
}
|
|
48
51
|
// After a reply is delivered, if context usage reaches this percent, send the
|
|
49
52
|
// user a one-time alert to compact. 0 disables the alert.
|
|
50
53
|
function contextAlertPct() { return Number(process.env.OPENCLAW_LARK_MULTI_AGENT_CONTEXT_ALERT_PCT || 80); }
|
|
@@ -101,8 +104,15 @@ export class FeishuBot {
|
|
|
101
104
|
lastRealDeliveryAt = new Map();
|
|
102
105
|
/** Per-chat: whether we've already sent the high-context alert this cycle. */
|
|
103
106
|
contextAlerted = new Map();
|
|
104
|
-
/** Active
|
|
107
|
+
/** Active pre-admission target for the current chat operation. */
|
|
105
108
|
activeDeliveryTargets = new Map();
|
|
109
|
+
/** Stable targets for concurrent runs in one chat. A steer admission can
|
|
110
|
+
* become an ordinary run while another run remains active. */
|
|
111
|
+
deliveryTargetsByRun = new Map();
|
|
112
|
+
/** Provisional native-steer correlations keyed by durable message row. If the
|
|
113
|
+
* message falls back to a normal run, cancel these before submission so a
|
|
114
|
+
* later identical steer cannot consume the stale observer/correlation. */
|
|
115
|
+
pendingSteerCleanups = new Map();
|
|
106
116
|
adminOpenId;
|
|
107
117
|
locale;
|
|
108
118
|
configPath;
|
|
@@ -225,7 +235,10 @@ export class FeishuBot {
|
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
237
|
catch (err) {
|
|
238
|
+
// Session/model setup is authoritative. Propagate policy/capability errors
|
|
239
|
+
// so the queue can notify the user and never continue on the wrong model.
|
|
228
240
|
console.warn(`[${this.config.name}] ensureSession error:`, err.message);
|
|
241
|
+
throw err;
|
|
229
242
|
}
|
|
230
243
|
this.initializedSessions.add(sessionKey);
|
|
231
244
|
// Subscribe to session events (proactive messages + tool calls)
|
|
@@ -237,11 +250,17 @@ export class FeishuBot {
|
|
|
237
250
|
const parsed = this.extractBridgeAttachments(text);
|
|
238
251
|
if (sourceType !== "verbose_transcript" && (parsed.text.trim() || parsed.attachments.length > 0))
|
|
239
252
|
this.cancelDelayedFailure(chatId);
|
|
240
|
-
const
|
|
253
|
+
const runTarget = meta?.runId ? this.deliveryTargetsByRun.get(this.deliveryRunTargetKey(chatId, meta.runId)) : undefined;
|
|
254
|
+
const candidateTarget = this.activeDeliveryTargets.get(chatId);
|
|
255
|
+
const activeTarget = runTarget || (candidateTarget && (!candidateTarget.runId || !meta?.runId || candidateTarget.runId === meta.runId)
|
|
256
|
+
? candidateTarget
|
|
257
|
+
: undefined);
|
|
241
258
|
try {
|
|
242
259
|
await this.enqueueAndDispatchDelivery(chatId, sourceType, this.deliverySourceId(sourceType === "verbose_transcript" ? "verbose" : "proactive", `${Date.now()}:${Math.random()}:${parsed.text.trim()}|${JSON.stringify(parsed.attachments)}`), parsed.text.trim(), parsed.attachments, activeTarget?.messageId, activeTarget ? `trigger:${activeTarget.triggerId}` : undefined, sourceType === "verbose_transcript" ? undefined : this.config.model);
|
|
243
260
|
if (sourceType !== "verbose_transcript" && (parsed.text.trim() || parsed.attachments.length > 0)) {
|
|
244
261
|
await activeTarget?.liveStatus?.complete().catch(() => { });
|
|
262
|
+
if (meta?.runId && runTarget)
|
|
263
|
+
this.releaseRunDeliveryTargetAfter(chatId, meta.runId, 30_000);
|
|
245
264
|
}
|
|
246
265
|
}
|
|
247
266
|
catch (err) {
|
|
@@ -923,44 +942,81 @@ export class FeishuBot {
|
|
|
923
942
|
// Wrap the message so the agent knows it is a NEW user message inserted
|
|
924
943
|
// mid-task. The live-status line and reactions still key off the original.
|
|
925
944
|
const steerWrapped = `${steerInjectionPrefix(this.isEn(chatId))}\n${cleanText}`;
|
|
945
|
+
// Register the consumption observer before submitting so a fast
|
|
946
|
+
// session.message cannot race past the Feishu reaction transition.
|
|
947
|
+
await this.addReaction(messageId, "Typing").catch(() => { });
|
|
948
|
+
pending.push({ messageId, emoji: "Typing", rowId: insertedId });
|
|
949
|
+
this.pendingAckMessages.set(chatId, pending);
|
|
950
|
+
const norm = cleanText.replace(/\s+/g, " ").trim();
|
|
951
|
+
const stopWatchingConsumption = this.openclawClient.onSteerConsumed(sessionKeyForSteer, steerWrapped, (consumedText) => {
|
|
952
|
+
const cn = (consumedText || "").replace(/\s+/g, " ").trim();
|
|
953
|
+
if (cn !== norm && !cn.includes(norm))
|
|
954
|
+
return false;
|
|
955
|
+
// Only transcript commitment/consumption transfers durable custody.
|
|
956
|
+
// Until this point the local pending trigger remains the fallback for
|
|
957
|
+
// detached dispatch rejection or an admission that never consumed.
|
|
958
|
+
if (insertedId > 0) {
|
|
959
|
+
this.store.clearPendingTrigger(this.config.name, chatId, insertedId);
|
|
960
|
+
this.pendingSteerCleanups.delete(insertedId);
|
|
961
|
+
}
|
|
962
|
+
void this.removeReaction(messageId, "Typing").catch(() => { });
|
|
963
|
+
void this.addReaction(messageId, "Get").catch(() => { });
|
|
964
|
+
const acks = this.pendingAckMessages.get(chatId) || [];
|
|
965
|
+
for (const a of acks)
|
|
966
|
+
if (a.messageId === messageId)
|
|
967
|
+
a.emoji = "Get";
|
|
968
|
+
this.pendingAckMessages.set(chatId, acks);
|
|
969
|
+
return true;
|
|
970
|
+
});
|
|
971
|
+
const previousActiveTarget = this.activeDeliveryTargets.get(chatId);
|
|
972
|
+
// Install the new Feishu delivery target before admission. OpenClaw may
|
|
973
|
+
// race from "old run still active" to "dispatch a new run" while
|
|
974
|
+
// handling queueMode=steer, and that new run can emit events before the
|
|
975
|
+
// RPC response reaches LMA. Either outcome now routes to this message.
|
|
976
|
+
const releaseSteerDeliveryTarget = this.setActiveDeliveryTarget(chatId, insertedId, messageId);
|
|
977
|
+
const provisionalTarget = this.activeDeliveryTargets.get(chatId);
|
|
926
978
|
const steer = await this.openclawClient.steer(sessionKeyForSteer, steerWrapped, cleanText).catch(() => ({ status: "unavailable" }));
|
|
927
979
|
if (steer.status === "steered") {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
this.pendingAckMessages.set(chatId, acks);
|
|
949
|
-
return true; // one-shot: matched, remove this callback
|
|
950
|
-
});
|
|
951
|
-
console.log(`[${this.config.name}] Agent busy for ${chatId.slice(-8)}, STEERED (awaiting consumption): "${cleanText.substring(0, 50)}..."`);
|
|
952
|
-
// The message is already in DB with a pending trigger. Clear that
|
|
953
|
-
// trigger so it is NOT re-run as a separate follow-up batch after the
|
|
954
|
-
// current run ends — it has already been injected into this run.
|
|
955
|
-
if (insertedId > 0)
|
|
956
|
-
this.store.clearPendingTrigger(this.config.name, chatId, insertedId);
|
|
980
|
+
if (insertedId > 0) {
|
|
981
|
+
this.pendingSteerCleanups.set(insertedId, () => {
|
|
982
|
+
stopWatchingConsumption();
|
|
983
|
+
if ("cancelPending" in steer)
|
|
984
|
+
steer.cancelPending?.();
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
if (provisionalTarget && steer.runId) {
|
|
988
|
+
provisionalTarget.runId = steer.runId;
|
|
989
|
+
this.bindRunDeliveryTarget(chatId, steer.runId, provisionalTarget);
|
|
990
|
+
this.releaseRunDeliveryTargetAfter(chatId, steer.runId, 30 * 60_000);
|
|
991
|
+
}
|
|
992
|
+
// A steer admission may become an ordinary new run. Keep its run-bound
|
|
993
|
+
// reply target long enough for a real tool-heavy run; stale targets are
|
|
994
|
+
// harmless because later runIds no longer match.
|
|
995
|
+
releaseSteerDeliveryTarget(30 * 60_000);
|
|
996
|
+
// RPC acknowledgment is provisional. Keep the durable pending trigger
|
|
997
|
+
// until session.message confirms consumption; otherwise the normal
|
|
998
|
+
// queue drains it after the current run.
|
|
999
|
+
console.log(`[${this.config.name}] Agent busy for ${chatId.slice(-8)}, steer accepted (awaiting transcript consumption): "${cleanText.substring(0, 50)}..."`);
|
|
957
1000
|
return;
|
|
958
1001
|
}
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1002
|
+
// Native steer was not accepted. Restore the previous run's routing
|
|
1003
|
+
// target and keep the already-created trigger/reaction for normal drain.
|
|
1004
|
+
stopWatchingConsumption();
|
|
1005
|
+
if ("cancelPending" in steer)
|
|
1006
|
+
steer.cancelPending?.();
|
|
1007
|
+
const currentTarget = this.activeDeliveryTargets.get(chatId);
|
|
1008
|
+
if (currentTarget && currentTarget.token === provisionalTarget?.token) {
|
|
1009
|
+
if (currentTarget.timer)
|
|
1010
|
+
clearTimeout(currentTarget.timer);
|
|
1011
|
+
this.activeDeliveryTargets.delete(chatId);
|
|
1012
|
+
if (previousActiveTarget) {
|
|
1013
|
+
const releaseRestoredTarget = this.setActiveDeliveryTarget(chatId, previousActiveTarget.triggerId, previousActiveTarget.messageId);
|
|
1014
|
+
const restored = this.activeDeliveryTargets.get(chatId);
|
|
1015
|
+
if (restored)
|
|
1016
|
+
restored.liveStatus = previousActiveTarget.liveStatus;
|
|
1017
|
+
releaseRestoredTarget();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
964
1020
|
console.log(`[${this.config.name}] Agent busy for ${chatId.slice(-8)} (steer=${steer.status}), queuing: "${cleanText.substring(0, 50)}..."`);
|
|
965
1021
|
return; // Message is in DB, will be picked up when agent finishes
|
|
966
1022
|
}
|
|
@@ -1190,6 +1246,16 @@ export class FeishuBot {
|
|
|
1190
1246
|
const runStartStopEpoch = this.stopEpoch.get(chatId) || 0;
|
|
1191
1247
|
const mergedContent = mergedTriggers.map((m) => m.content).join("\n");
|
|
1192
1248
|
const mergedTriggerIds = mergedTriggers.map((m) => m.id || 0).filter(Boolean);
|
|
1249
|
+
// These rows are now taking the ordinary queue path. Remove any stale
|
|
1250
|
+
// provisional native-steer correlation before chat.send so a later
|
|
1251
|
+
// identical steer cannot acknowledge the wrong Feishu message.
|
|
1252
|
+
for (const id of mergedTriggerIds) {
|
|
1253
|
+
const cleanup = this.pendingSteerCleanups.get(id);
|
|
1254
|
+
if (cleanup) {
|
|
1255
|
+
cleanup();
|
|
1256
|
+
this.pendingSteerCleanups.delete(id);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1193
1259
|
const droppedTriggerIds = droppedMergedTriggers.map((m) => m.id || 0).filter(Boolean);
|
|
1194
1260
|
const mergedTriggerIdSet = new Set(mergedTriggerIds);
|
|
1195
1261
|
const completedTriggerIdSet = new Set([...droppedTriggerIds, ...mergedTriggerIds]);
|
|
@@ -1238,7 +1304,28 @@ export class FeishuBot {
|
|
|
1238
1304
|
}
|
|
1239
1305
|
const processedMessages = [...contextMsgs, ...mergedTriggers].filter((m) => m.id);
|
|
1240
1306
|
const queueStartedAt = Date.now();
|
|
1241
|
-
|
|
1307
|
+
let sessionKey;
|
|
1308
|
+
try {
|
|
1309
|
+
sessionKey = await this.ensureSession(chatId);
|
|
1310
|
+
}
|
|
1311
|
+
catch (err) {
|
|
1312
|
+
// Model policy/capability failures happen before chat.send. Clear the
|
|
1313
|
+
// busy marker, report the actionable error, and retire only this batch
|
|
1314
|
+
// so the chat cannot remain wedged indefinitely.
|
|
1315
|
+
this.busyChats.set(chatId, 0);
|
|
1316
|
+
const errorText = this.formatUserVisibleError(err);
|
|
1317
|
+
if (lastHuman.messageId) {
|
|
1318
|
+
await this.enqueueAndDispatchDelivery(chatId, "provider_error", `trigger:${triggerId}:session-setup-error`, errorText, [], lastHuman.messageId, `trigger:${triggerId}:session-setup-error`).catch(() => { });
|
|
1319
|
+
}
|
|
1320
|
+
for (const id of mergedTriggerIds)
|
|
1321
|
+
this.store.clearPendingTrigger(this.config.name, chatId, id);
|
|
1322
|
+
this.store.markMessagesSynced(this.config.name, chatId, mergedTriggerIds, `${this.config.name}:${chatId}:${triggerId}:session-setup-failed`);
|
|
1323
|
+
if (mergedTriggerIds.length > 0)
|
|
1324
|
+
this.store.markSynced(this.config.name, chatId, Math.max(...mergedTriggerIds));
|
|
1325
|
+
const pendingAcks = this.pendingAckMessages.get(chatId) || [];
|
|
1326
|
+
this.pendingAckMessages.set(chatId, pendingAcks.filter((ack) => !completedTriggerIdSet.has(ack.rowId)));
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1242
1329
|
console.log(`[${this.config.name}] Sending ${mergedTriggers.length} trigger(s) as 1 run to OpenClaw for ${chatId.slice(-8)} (context=${contextMsgs.length}, dropped=${droppedTriggerIds.length})`);
|
|
1243
1330
|
// Update reactions: queued messages → sent (GET/了解)
|
|
1244
1331
|
const pendingAcks = this.pendingAckMessages.get(chatId) || [];
|
|
@@ -1304,6 +1391,12 @@ export class FeishuBot {
|
|
|
1304
1391
|
},
|
|
1305
1392
|
onSubmitted: (runId) => {
|
|
1306
1393
|
runAcceptedByOpenClaw = true;
|
|
1394
|
+
const target = this.activeDeliveryTargets.get(chatId);
|
|
1395
|
+
if (target?.triggerId === triggerId) {
|
|
1396
|
+
target.runId = runId;
|
|
1397
|
+
this.bindRunDeliveryTarget(chatId, runId, target);
|
|
1398
|
+
this.releaseRunDeliveryTargetAfter(chatId, runId, 30 * 60_000);
|
|
1399
|
+
}
|
|
1307
1400
|
// Refine state for logs/idempotent sync rows when OpenClaw returns
|
|
1308
1401
|
// a runId. markSubmittedBatch is idempotent due to local flags and
|
|
1309
1402
|
// INSERT OR IGNORE in message_sync.
|
|
@@ -2012,18 +2105,48 @@ export class FeishuBot {
|
|
|
2012
2105
|
clearTimeout(timer);
|
|
2013
2106
|
this.delayedFailureTimers.delete(chatId);
|
|
2014
2107
|
}
|
|
2108
|
+
deliveryRunTargetKey(chatId, runId) {
|
|
2109
|
+
return `${chatId}\u0000${runId}`;
|
|
2110
|
+
}
|
|
2111
|
+
bindRunDeliveryTarget(chatId, runId, target) {
|
|
2112
|
+
if (!runId)
|
|
2113
|
+
return;
|
|
2114
|
+
const key = this.deliveryRunTargetKey(chatId, runId);
|
|
2115
|
+
const previous = this.deliveryTargetsByRun.get(key);
|
|
2116
|
+
if (previous?.timer)
|
|
2117
|
+
clearTimeout(previous.timer);
|
|
2118
|
+
this.deliveryTargetsByRun.set(key, {
|
|
2119
|
+
triggerId: target.triggerId,
|
|
2120
|
+
messageId: target.messageId,
|
|
2121
|
+
liveStatus: target.liveStatus,
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
releaseRunDeliveryTargetAfter(chatId, runId, delayMs) {
|
|
2125
|
+
const key = this.deliveryRunTargetKey(chatId, runId);
|
|
2126
|
+
const target = this.deliveryTargetsByRun.get(key);
|
|
2127
|
+
if (!target)
|
|
2128
|
+
return;
|
|
2129
|
+
if (target.timer)
|
|
2130
|
+
clearTimeout(target.timer);
|
|
2131
|
+
const timer = setTimeout(() => {
|
|
2132
|
+
if (this.deliveryTargetsByRun.get(key)?.timer === timer)
|
|
2133
|
+
this.deliveryTargetsByRun.delete(key);
|
|
2134
|
+
}, delayMs);
|
|
2135
|
+
timer.unref?.();
|
|
2136
|
+
target.timer = timer;
|
|
2137
|
+
}
|
|
2015
2138
|
setActiveDeliveryTarget(chatId, triggerId, messageId) {
|
|
2016
2139
|
const existing = this.activeDeliveryTargets.get(chatId);
|
|
2017
2140
|
if (existing?.timer)
|
|
2018
2141
|
clearTimeout(existing.timer);
|
|
2019
2142
|
const token = Symbol(`${chatId}:${triggerId}`);
|
|
2020
2143
|
this.activeDeliveryTargets.set(chatId, { triggerId, messageId, token });
|
|
2021
|
-
return () => {
|
|
2144
|
+
return (delayMs = 60_000) => {
|
|
2022
2145
|
const timer = setTimeout(() => {
|
|
2023
2146
|
const current = this.activeDeliveryTargets.get(chatId);
|
|
2024
2147
|
if (current?.token === token)
|
|
2025
2148
|
this.activeDeliveryTargets.delete(chatId);
|
|
2026
|
-
},
|
|
2149
|
+
}, delayMs);
|
|
2027
2150
|
const current = this.activeDeliveryTargets.get(chatId);
|
|
2028
2151
|
if (current?.token === token)
|
|
2029
2152
|
current.timer = timer;
|
|
@@ -2650,8 +2773,8 @@ export class FeishuBot {
|
|
|
2650
2773
|
};
|
|
2651
2774
|
}
|
|
2652
2775
|
// Running: phase line + ticking elapsed footer.
|
|
2653
|
-
const phaseLine = view.phase === "
|
|
2654
|
-
? (en ? "🧹
|
|
2776
|
+
const phaseLine = view.phase === "transcript-trim"
|
|
2777
|
+
? (en ? "🧹 Semantic compaction unavailable — trimming transcript safely…" : "🧹 语义压缩不可用,正在安全裁剪转录…")
|
|
2655
2778
|
: (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
|
|
2656
2779
|
const footer = en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`;
|
|
2657
2780
|
return {
|
|
@@ -3409,17 +3532,16 @@ export class FeishuBot {
|
|
|
3409
3532
|
* Handle /compact command: compress session context.
|
|
3410
3533
|
*/
|
|
3411
3534
|
/**
|
|
3412
|
-
* Compact a session with a fallback chain:
|
|
3413
|
-
* 1.
|
|
3414
|
-
* 2. If
|
|
3415
|
-
*
|
|
3416
|
-
*
|
|
3417
|
-
*
|
|
3418
|
-
*
|
|
3419
|
-
*
|
|
3535
|
+
* Compact a session with a Gateway-owned fallback chain:
|
|
3536
|
+
* 1. Semantic compaction through `sessions.compact`.
|
|
3537
|
+
* 2. If summarization cannot run, ask the same RPC to retain only a bounded
|
|
3538
|
+
* transcript tail via `maxLines`. This works with legacy JSONL and the
|
|
3539
|
+
* SQLite session store introduced in OpenClaw 2026.8.x, and preserves
|
|
3540
|
+
* OpenClaw's write locks, transcript invariants, and migration ownership.
|
|
3541
|
+
*
|
|
3542
|
+
* LMA deliberately never opens or rewrites OpenClaw transcript storage.
|
|
3420
3543
|
*/
|
|
3421
3544
|
async compactWithFallback(sessionKey, onPhase) {
|
|
3422
|
-
// 1. Native compaction first.
|
|
3423
3545
|
let nativeReason;
|
|
3424
3546
|
try {
|
|
3425
3547
|
const res = await this.openclawClient.compactSession(sessionKey);
|
|
@@ -3430,48 +3552,41 @@ export class FeishuBot {
|
|
|
3430
3552
|
catch (err) {
|
|
3431
3553
|
nativeReason = err.message;
|
|
3432
3554
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
// slow→still-working transition the user most needs to see).
|
|
3436
|
-
onPhase?.("tool-trim");
|
|
3437
|
-
let sessionId;
|
|
3555
|
+
onPhase?.("transcript-trim");
|
|
3556
|
+
const maxLines = compactFallbackMaxLines();
|
|
3438
3557
|
try {
|
|
3439
|
-
const
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
return { compacted: false, method: "none", reason: trim.reason || "tool-trim failed" };
|
|
3558
|
+
const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines });
|
|
3559
|
+
if (trimmed?.compacted === true) {
|
|
3560
|
+
const kept = typeof trimmed?.kept === "number" ? trimmed.kept : maxLines;
|
|
3561
|
+
return {
|
|
3562
|
+
compacted: true,
|
|
3563
|
+
method: "transcript-trim",
|
|
3564
|
+
detail: `kept ${kept} transcript events`,
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
const trimReason = typeof trimmed?.reason === "string" ? trimmed.reason : undefined;
|
|
3568
|
+
return {
|
|
3569
|
+
compacted: false,
|
|
3570
|
+
method: "none",
|
|
3571
|
+
reason: trimReason || nativeReason || "transcript trim made no change",
|
|
3572
|
+
};
|
|
3455
3573
|
}
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3574
|
+
catch (err) {
|
|
3575
|
+
const trimReason = err.message;
|
|
3576
|
+
return {
|
|
3577
|
+
compacted: false,
|
|
3578
|
+
method: "none",
|
|
3579
|
+
reason: nativeReason
|
|
3580
|
+
? `semantic compaction: ${nativeReason}; transcript trim: ${trimReason}`
|
|
3581
|
+
: trimReason,
|
|
3582
|
+
};
|
|
3461
3583
|
}
|
|
3462
|
-
const pct = before > 0 ? Math.round((1 - after / before) * 100) : 0;
|
|
3463
|
-
const mb = (n) => `${(n / 1024 / 1024).toFixed(1)}M`;
|
|
3464
|
-
return {
|
|
3465
|
-
compacted: true,
|
|
3466
|
-
method: "tool-trim",
|
|
3467
|
-
detail: `${mb(before)}→${mb(after)} (-${pct}%)`,
|
|
3468
|
-
};
|
|
3469
3584
|
}
|
|
3470
3585
|
async handleCompactCommand(chatId, messageId) {
|
|
3471
3586
|
const sessionKey = this.getSessionKey(chatId);
|
|
3472
3587
|
const en = this.isEn(chatId);
|
|
3473
3588
|
// Large sessions can take tens of seconds to compact (native compaction may
|
|
3474
|
-
// even time out before the
|
|
3589
|
+
// even time out before the transcript-trim fallback kicks in). Show a ticking
|
|
3475
3590
|
// "compacting…" card so the user can SEE it is working instead of staring at
|
|
3476
3591
|
// nothing and assuming nothing is happening. The card is created lazily, so
|
|
3477
3592
|
// a fast compaction that finishes quickly never flashes a card at all.
|
|
@@ -3483,19 +3598,24 @@ export class FeishuBot {
|
|
|
3483
3598
|
progress.start();
|
|
3484
3599
|
try {
|
|
3485
3600
|
const r = await this.compactWithFallback(sessionKey, (phase) => {
|
|
3486
|
-
if (phase === "
|
|
3487
|
-
void progress.
|
|
3601
|
+
if (phase === "transcript-trim")
|
|
3602
|
+
void progress.toTranscriptTrim().catch(() => { });
|
|
3488
3603
|
});
|
|
3489
3604
|
if (r.compacted) {
|
|
3490
3605
|
// If a card was shown, patch it in place; otherwise (fast finish, no card)
|
|
3491
3606
|
// fall back to a normal reply so the user still gets confirmation.
|
|
3492
|
-
const
|
|
3493
|
-
|
|
3607
|
+
const localizedTrimDetail = r.detail
|
|
3608
|
+
?.replace("kept ", en ? "kept " : "保留最近 ")
|
|
3609
|
+
.replace(" transcript events", en ? " transcript events" : " 条转录事件");
|
|
3610
|
+
const detail = r.method === "transcript-trim" && localizedTrimDetail
|
|
3611
|
+
? (en ? `transcript trim: ${localizedTrimDetail}` : `转录裁剪:${localizedTrimDetail}`)
|
|
3494
3612
|
: (en ? "native" : "原生压缩");
|
|
3495
3613
|
await progress.done(detail);
|
|
3496
3614
|
if (!progress.id) {
|
|
3497
|
-
await this.replyMessage(messageId, r.method === "
|
|
3498
|
-
? (en
|
|
3615
|
+
await this.replyMessage(messageId, r.method === "transcript-trim"
|
|
3616
|
+
? (en
|
|
3617
|
+
? `✅ Session compacted (transcript trim${localizedTrimDetail ? ": " + localizedTrimDetail : ""})`
|
|
3618
|
+
: `✅ Session 已压缩(转录裁剪${localizedTrimDetail ? ":" + localizedTrimDetail : ""})`)
|
|
3499
3619
|
: (en ? `✅ Session compacted` : `✅ Session 已压缩`));
|
|
3500
3620
|
}
|
|
3501
3621
|
}
|
|
@@ -21,7 +21,7 @@ export type ChatAttachment = {
|
|
|
21
21
|
fileName?: string;
|
|
22
22
|
content: string;
|
|
23
23
|
};
|
|
24
|
-
export declare const GATEWAY_PROTOCOL_MIN =
|
|
24
|
+
export declare const GATEWAY_PROTOCOL_MIN = 4;
|
|
25
25
|
export declare const GATEWAY_PROTOCOL_MAX = 4;
|
|
26
26
|
/**
|
|
27
27
|
* OpenClaw Gateway WebSocket client.
|
|
@@ -40,11 +40,14 @@ export declare class OpenClawClient {
|
|
|
40
40
|
/** Callbacks for tool events (verbose mode) */
|
|
41
41
|
private toolEventCallbacks;
|
|
42
42
|
private lastToolNamesByItemId;
|
|
43
|
+
/** Deduplicates 2026.8 canonical `agent.stream=tool` events against legacy
|
|
44
|
+
* `stream=item` mirrors when a runtime emits both representations. */
|
|
45
|
+
private recentToolEventKeys;
|
|
43
46
|
private sessionMessageCallbacks;
|
|
44
47
|
private progressCallbacks;
|
|
45
|
-
/** Per-session steered messages awaiting consumption confirmation.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
+
/** Per-session steered messages awaiting consumption confirmation. Each
|
|
49
|
+
* accepted message is retained independently so identical/overlapping text
|
|
50
|
+
* cannot overwrite another correlation. */
|
|
48
51
|
private pendingSteerTexts;
|
|
49
52
|
/** Sessions where verbose mode should deliver visible transcript text even when mixed with tool blocks. */
|
|
50
53
|
private verboseTranscriptSessions;
|
|
@@ -57,9 +60,11 @@ export declare class OpenClawClient {
|
|
|
57
60
|
/** Session keys whose transcript/session.message updates are currently suppressed. */
|
|
58
61
|
private suppressedSessions;
|
|
59
62
|
private suppressedSessionTimers;
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
/** Run ids whose final delivery is owned by chatSend/collectReply. Delivery
|
|
64
|
+
* ownership must be run-scoped: a concurrent queueMode=steer admission may
|
|
65
|
+
* fall through to a separate ordinary run in the same session. */
|
|
66
|
+
private ownedDeliveryRuns;
|
|
67
|
+
private ownedDeliveryRunTimers;
|
|
63
68
|
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
64
69
|
private mutedProactiveSessions;
|
|
65
70
|
private mutedProactiveSessionCounts;
|
|
@@ -83,6 +88,7 @@ export declare class OpenClawClient {
|
|
|
83
88
|
private extractVisibleAssistantText;
|
|
84
89
|
private isVerboseTranscriptEnabled;
|
|
85
90
|
private clearVerboseAssistantState;
|
|
91
|
+
private claimToolEvent;
|
|
86
92
|
private pruneVerboseCaches;
|
|
87
93
|
private flushVerboseAssistantState;
|
|
88
94
|
private handleVerboseAssistantStream;
|
|
@@ -128,21 +134,19 @@ export declare class OpenClawClient {
|
|
|
128
134
|
label?: string;
|
|
129
135
|
}): Promise<any>;
|
|
130
136
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* - "no_active_run" -> no active run; caller should fall back to normal queueing
|
|
139
|
-
* - "rejected" -> active run refused the injection (compacting/not streaming)
|
|
140
|
-
* - "unavailable" -> the lma-steer plugin/method is not available (e.g. not
|
|
141
|
-
* installed or gateway not restarted); caller falls back.
|
|
137
|
+
* Submit a mid-run message through OpenClaw's public atomic steer path.
|
|
138
|
+
* OpenClaw 2026.8 deprecated the synchronous plugin primitive previously used
|
|
139
|
+
* by `lma-steer`: its boolean only represented immediate eligibility and could
|
|
140
|
+
* still be rejected asynchronously. `chat.send queueMode=steer` owns admission
|
|
141
|
+
* and transcript persistence. LMA still treats the RPC result as provisional;
|
|
142
|
+
* the Feishu reaction flips to Get only after a matching `session.message`
|
|
143
|
+
* confirms that the input was committed/consumed.
|
|
142
144
|
*/
|
|
143
145
|
steer(sessionKey: string, text: string, displayText?: string): Promise<{
|
|
144
|
-
status: "steered" | "
|
|
145
|
-
|
|
146
|
+
status: "steered" | "unavailable";
|
|
147
|
+
runId?: string;
|
|
148
|
+
/** Cancel provisional transcript correlation when LMA falls back to a normal run. */
|
|
149
|
+
cancelPending?: () => void;
|
|
146
150
|
}>;
|
|
147
151
|
/** Callbacks fired when a steered message is confirmed consumed by the model
|
|
148
152
|
* (matching sessionUser event). Keyed by bare/short session key. Multiple may
|
|
@@ -154,8 +158,9 @@ export declare class OpenClawClient {
|
|
|
154
158
|
* is actually consumed by the active run. The bridge uses this to flip the
|
|
155
159
|
* Feishu reaction from "Typing" (awaiting insertion) to "Get" (inserted).
|
|
156
160
|
* Return true from the callback to consume/remove it (one-shot). */
|
|
157
|
-
onSteerConsumed(sessionKey: string, cb: (text: string) => boolean | void): void;
|
|
161
|
+
onSteerConsumed(sessionKey: string, matchText: string, cb: (text: string) => boolean | void): () => void;
|
|
158
162
|
private shortKey;
|
|
163
|
+
private canonicalSessionKey;
|
|
159
164
|
private registerPendingSteer;
|
|
160
165
|
/**
|
|
161
166
|
* Called when a sessionUser event is seen for a session. If its text matches a
|
|
@@ -175,7 +180,9 @@ export declare class OpenClawClient {
|
|
|
175
180
|
ensureModel(sessionKey: string, expectedModel: string): Promise<boolean>;
|
|
176
181
|
deleteSession(key: string, deleteTranscript?: boolean): Promise<any>;
|
|
177
182
|
resetSession(key: string): Promise<any>;
|
|
178
|
-
compactSession(key: string
|
|
183
|
+
compactSession(key: string, options?: {
|
|
184
|
+
maxLines?: number;
|
|
185
|
+
}): Promise<any>;
|
|
179
186
|
/**
|
|
180
187
|
* Send a message to a session and get the agent reply.
|
|
181
188
|
* deliver=false prevents OpenClaw from auto-posting to channels.
|
|
@@ -187,9 +194,8 @@ export declare class OpenClawClient {
|
|
|
187
194
|
private emitProactiveForSession;
|
|
188
195
|
private suppressSessionKeys;
|
|
189
196
|
private releaseSuppressedSessionKeysAfter;
|
|
190
|
-
private
|
|
191
|
-
private
|
|
192
|
-
private isOwnedDeliverySession;
|
|
197
|
+
private ownDeliveryRun;
|
|
198
|
+
private releaseOwnedDeliveryRunAfter;
|
|
193
199
|
private addMutedProactiveKey;
|
|
194
200
|
private releaseMutedProactiveKey;
|
|
195
201
|
muteProactiveDelivery(sessionKey: string): (delayMs?: number) => void;
|
|
@@ -253,6 +259,7 @@ export declare class OpenClawClient {
|
|
|
253
259
|
*/
|
|
254
260
|
subscribeSession(sessionKey: string, onMessage: (text: string, meta?: {
|
|
255
261
|
sourceType?: string;
|
|
262
|
+
runId?: string;
|
|
256
263
|
}) => void): Promise<void>;
|
|
257
264
|
setVerboseTranscriptDelivery(sessionKey: string, enabled: boolean): void;
|
|
258
265
|
unsubscribeSession(sessionKey: string): Promise<void>;
|