openclaw-lark-multi-agent 1.4.12 → 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.
- package/dist/compact-progress.d.ts +3 -1
- package/dist/compact-progress.js +6 -0
- package/dist/config.d.ts +2 -0
- package/dist/feishu-bot.d.ts +20 -11
- package/dist/feishu-bot.js +298 -61
- package/dist/live-status.d.ts +5 -0
- package/dist/live-status.js +2 -0
- package/dist/message-store.d.ts +30 -0
- package/dist/message-store.js +94 -0
- package/dist/openclaw-client.d.ts +16 -4
- package/dist/openclaw-client.js +151 -22
- package/dist/terminal-errors.d.ts +49 -0
- package/dist/terminal-errors.js +116 -0
- package/package.json +1 -1
package/dist/feishu-bot.js
CHANGED
|
@@ -6,6 +6,7 @@ import { InactiveRunObservation } from "./openclaw-client.js";
|
|
|
6
6
|
import { SessionWaitPaused, isWaitTimeout, normalizeSessionRuntimeStatus, formatSessionFooter, FOREGROUND_WAIT_MS } from "./session-status.js";
|
|
7
7
|
import { LiveStatusController } from "./live-status.js";
|
|
8
8
|
import { CompactProgressController } from "./compact-progress.js";
|
|
9
|
+
import { errorIdentity } from "./terminal-errors.js";
|
|
9
10
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
10
11
|
import { basename, extname, join, resolve } from "path";
|
|
11
12
|
import { getBridgeAttachmentsDir, getDataDir } from "./paths.js";
|
|
@@ -43,12 +44,12 @@ function steerInjectionPrefix(en) {
|
|
|
43
44
|
|| "[New message inserted by the user mid-task; please handle it together with the current task]";
|
|
44
45
|
return en ? enText : zh;
|
|
45
46
|
}
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
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.
|
|
49
50
|
function compactFallbackMaxLines() {
|
|
50
|
-
const value = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_FALLBACK_MAX_LINES ||
|
|
51
|
-
return Number.isFinite(value) ? Math.max(20, Math.floor(value)) :
|
|
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;
|
|
52
53
|
}
|
|
53
54
|
// After a reply is delivered, if context usage reaches this percent, send the
|
|
54
55
|
// user a one-time alert to compact. 0 disables the alert.
|
|
@@ -117,6 +118,8 @@ export class FeishuBot {
|
|
|
117
118
|
* message falls back to a normal run, cancel these before submission so a
|
|
118
119
|
* later identical steer cannot consume the stale observer/correlation. */
|
|
119
120
|
pendingSteerCleanups = new Map();
|
|
121
|
+
errorNoticeTimers = new Map();
|
|
122
|
+
errorNoticeChecks = new Map();
|
|
120
123
|
adminOpenId;
|
|
121
124
|
locale;
|
|
122
125
|
configPath;
|
|
@@ -209,7 +212,6 @@ export class FeishuBot {
|
|
|
209
212
|
const corrected = await this.openclawClient.ensureModel(sessionKey, this.config.model);
|
|
210
213
|
if (corrected) {
|
|
211
214
|
console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
|
|
212
|
-
await this.notifyModelDrift(chatId, sessionKey);
|
|
213
215
|
}
|
|
214
216
|
return sessionKey;
|
|
215
217
|
}
|
|
@@ -222,7 +224,6 @@ export class FeishuBot {
|
|
|
222
224
|
const corrected = await this.openclawClient.ensureModel(sessionKey, this.config.model);
|
|
223
225
|
if (corrected) {
|
|
224
226
|
console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
|
|
225
|
-
await this.notifyModelDrift(chatId, sessionKey);
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
else {
|
|
@@ -249,6 +250,19 @@ export class FeishuBot {
|
|
|
249
250
|
this.openclawClient.setVerboseTranscriptDelivery(sessionKey, this.store.getBotVerbose(this.config.name, chatId));
|
|
250
251
|
await this.openclawClient.subscribeSession(sessionKey, async (text, meta) => {
|
|
251
252
|
try {
|
|
253
|
+
let provenance = meta?.error;
|
|
254
|
+
if (!provenance && meta?.genericErrorCandidate && meta.runId && meta.sessionKey) {
|
|
255
|
+
provenance = this.store.getErrorNotice(this.config.name, chatId, errorIdentity(meta.sessionKey, meta.runId))?.provenance;
|
|
256
|
+
}
|
|
257
|
+
if (meta?.sourceType === "run_error" || provenance) {
|
|
258
|
+
if (provenance) {
|
|
259
|
+
this.store.observeRunError(this.config.name, chatId, provenance);
|
|
260
|
+
await this.reconcileErrorNotices(chatId);
|
|
261
|
+
}
|
|
262
|
+
else
|
|
263
|
+
console.warn(`[${this.config.name}] Uncorrelated run_error retained in logs; no finality evidence`);
|
|
264
|
+
return; // errors never cancel pending results or complete live cards
|
|
265
|
+
}
|
|
252
266
|
const sourceType = meta?.sourceType === "verbose_transcript" ? "verbose_transcript" : "assistant_visible";
|
|
253
267
|
console.log(`[${this.config.name}] ${sourceType === "verbose_transcript" ? "Verbose transcript" : "Proactive message"} for ${chatId.slice(-8)}`);
|
|
254
268
|
const parsed = this.extractBridgeAttachments(text);
|
|
@@ -260,7 +274,7 @@ export class FeishuBot {
|
|
|
260
274
|
? candidateTarget
|
|
261
275
|
: undefined);
|
|
262
276
|
try {
|
|
263
|
-
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);
|
|
277
|
+
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, meta);
|
|
264
278
|
if (sourceType !== "verbose_transcript" && (parsed.text.trim() || parsed.attachments.length > 0)) {
|
|
265
279
|
await activeTarget?.liveStatus?.complete().catch(() => { });
|
|
266
280
|
if (meta?.runId && runTarget)
|
|
@@ -363,6 +377,7 @@ export class FeishuBot {
|
|
|
363
377
|
}
|
|
364
378
|
// Re-subscribe to existing sessions
|
|
365
379
|
await this.ensureSession(chat.chatId);
|
|
380
|
+
drainTasks.push(this.reconcileErrorNotices(chat.chatId));
|
|
366
381
|
// Drain only messages that were explicitly marked as reply triggers.
|
|
367
382
|
// Context-only messages should not start an OpenClaw run after restart.
|
|
368
383
|
const pendingTriggerIds = this.store.getPendingTriggerIds(this.config.name, chat.chatId);
|
|
@@ -666,7 +681,7 @@ export class FeishuBot {
|
|
|
666
681
|
`━━━━━━━━━━━━━━━━━━`,
|
|
667
682
|
`桥接层命令(单斜杠,由 openclaw-lark-multi-agent 本地处理)`,
|
|
668
683
|
`📊 /status — 查看当前模型、Token 用量、Session 状态`,
|
|
669
|
-
`🧹 /compact —
|
|
684
|
+
`🧹 /compact [N|maxLines=N|semantic] — 裁剪转录尾部(默认,保留最近事件);N 指定保留条数;semantic 走 LLM 总结`,
|
|
670
685
|
`🔄 /reset — 重置当前 bot 的 OpenClaw session`,
|
|
671
686
|
`⏹️ /stop — 强制停止当前 bot 在本聊天的卡死 run,解锁队列`,
|
|
672
687
|
`🔊 /verbose — 开关当前聊天里的 Tool Call 显示`,
|
|
@@ -712,7 +727,8 @@ export class FeishuBot {
|
|
|
712
727
|
}
|
|
713
728
|
if (commandName === "/compact") {
|
|
714
729
|
await this.ensureSession(chatId);
|
|
715
|
-
|
|
730
|
+
const compactArgs = cleanText.trim().split(/\s+/).slice(1).join(" ").trim();
|
|
731
|
+
await this.handleCompactCommand(chatId, messageId, compactArgs);
|
|
716
732
|
markCommandSynced();
|
|
717
733
|
return;
|
|
718
734
|
}
|
|
@@ -1343,6 +1359,7 @@ export class FeishuBot {
|
|
|
1343
1359
|
}, {
|
|
1344
1360
|
botName: this.config.name,
|
|
1345
1361
|
model: this.config.model,
|
|
1362
|
+
thinkingLevel: await this.fetchThinkingLabel(sessionKey),
|
|
1346
1363
|
locale: this.isEn(chatId) ? "en" : "zh",
|
|
1347
1364
|
})
|
|
1348
1365
|
: undefined;
|
|
@@ -1871,7 +1888,12 @@ export class FeishuBot {
|
|
|
1871
1888
|
const core = en
|
|
1872
1889
|
? `${emoji} ${meta.toolCalls} tool call${meta.toolCalls === 1 ? "" : "s"} · ⏱ ${meta.elapsed}`
|
|
1873
1890
|
: `${emoji} 累计${meta.toolCalls} 次工具调用 · ⏱ 耗时${meta.elapsed}`;
|
|
1874
|
-
|
|
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)}`;
|
|
1875
1897
|
}
|
|
1876
1898
|
/** Best-effort terminal cleanup for status-only rows and legacy v1.4.3
|
|
1877
1899
|
* outbox entries that once targeted the live-status card. */
|
|
@@ -1881,6 +1903,7 @@ export class FeishuBot {
|
|
|
1881
1903
|
lines: [],
|
|
1882
1904
|
elapsed: meta.elapsed,
|
|
1883
1905
|
model: meta.model,
|
|
1906
|
+
thinkingLevel: meta.thinkingLevel,
|
|
1884
1907
|
toolCalls: meta.toolCalls,
|
|
1885
1908
|
noReply: Boolean(meta.noReply),
|
|
1886
1909
|
state: "done",
|
|
@@ -2263,23 +2286,33 @@ export class FeishuBot {
|
|
|
2263
2286
|
const sourceId = `discussion-system:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
2264
2287
|
await this.enqueueAndDispatchDelivery(chatId, "discussion_system", sourceId, text);
|
|
2265
2288
|
}
|
|
2266
|
-
async enqueueAndDispatchDelivery(chatId, sourceType, sourceId, text, attachments = [], replyToMessageId, deliveryKey, finalReplyModel) {
|
|
2289
|
+
async enqueueAndDispatchDelivery(chatId, sourceType, sourceId, text, attachments = [], replyToMessageId, deliveryKey, finalReplyModel, provenance, errorNoticeId) {
|
|
2267
2290
|
if (!text.trim() && attachments.length === 0)
|
|
2268
2291
|
return;
|
|
2269
2292
|
const attachmentsJson = JSON.stringify(attachments);
|
|
2270
2293
|
const normalizedPayload = `${text.trim()}|${attachmentsJson}`;
|
|
2271
2294
|
const contentHash = this.stableHash(normalizedPayload);
|
|
2272
2295
|
const finalDeliveryKey = deliveryKey || sourceId;
|
|
2273
|
-
const deliveryMetaJson =
|
|
2274
|
-
?
|
|
2275
|
-
:
|
|
2296
|
+
const deliveryMetaJson = JSON.stringify({
|
|
2297
|
+
...(finalReplyModel?.trim() ? { model: finalReplyModel.trim() } : {}),
|
|
2298
|
+
...(provenance ? { provenance } : {}), ...(errorNoticeId ? { errorNoticeId } : {}),
|
|
2299
|
+
});
|
|
2300
|
+
const correlatedFinal = sourceType === "assistant_visible" && provenance?.final && Boolean(provenance.runId && provenance.sessionKey);
|
|
2301
|
+
if (correlatedFinal) {
|
|
2302
|
+
const identity = { sessionKey: provenance.sessionKey, runId: provenance.runId };
|
|
2303
|
+
this.store.promoteExactRunFinal(this.config.name, chatId, identity.sessionKey, identity.runId, contentHash, text, attachmentsJson);
|
|
2304
|
+
if (!deliveryKey && this.store.hasExactRunFinalPayload(this.config.name, chatId, identity, text, attachmentsJson)) {
|
|
2305
|
+
await this.dispatchPendingDeliveries(chatId);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2276
2309
|
if (sourceType === "verbose_transcript") {
|
|
2277
2310
|
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000, ["verbose_transcript"]))
|
|
2278
2311
|
return;
|
|
2279
2312
|
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8, ["verbose_transcript"]))
|
|
2280
2313
|
return;
|
|
2281
2314
|
}
|
|
2282
|
-
else if (!deliveryKey) {
|
|
2315
|
+
else if (!deliveryKey && !correlatedFinal) {
|
|
2283
2316
|
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000, [sourceType]))
|
|
2284
2317
|
return;
|
|
2285
2318
|
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8, [sourceType]))
|
|
@@ -2306,6 +2339,15 @@ export class FeishuBot {
|
|
|
2306
2339
|
// answer again on a second card. The original outbox row remains the sole
|
|
2307
2340
|
// owner of text delivery; a different newly-frozen card is only collapsed.
|
|
2308
2341
|
const existing = this.store.getDeliveryByKey(this.config.name, chatId, finalDeliveryKey);
|
|
2342
|
+
if (sourceType === "run_error") {
|
|
2343
|
+
// A withdrawn unsent notice may become terminal later. Keep its one
|
|
2344
|
+
// task key; never use the result recovery/correction/attachment paths.
|
|
2345
|
+
if (existing?.status === "failed" && !existing.textDelivered && existing.attempts < DELIVERY_MAX_ATTEMPTS) {
|
|
2346
|
+
this.store.renewUnsentErrorDelivery(existing.id, text, contentHash, deliveryMetaJson);
|
|
2347
|
+
}
|
|
2348
|
+
await this.dispatchPendingDeliveries(chatId);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2309
2351
|
// A failed owner must not permanently poison a logical trigger. Re-enqueue
|
|
2310
2352
|
// the payload under a deterministic recovery key so a later authoritative
|
|
2311
2353
|
// delivery gets a fresh retry budget.
|
|
@@ -2408,6 +2450,124 @@ export class FeishuBot {
|
|
|
2408
2450
|
this.lastRealDeliveryAt.set(chatId, Date.now());
|
|
2409
2451
|
}
|
|
2410
2452
|
}
|
|
2453
|
+
async inspectErrorNotice(record) {
|
|
2454
|
+
if (this.store.hasRunFinalDelivery(this.config.name, record.chatId, record.provenance, true)) {
|
|
2455
|
+
return { state: "suppressed", reason: "exact_run_final_platform_delivery_confirmed" };
|
|
2456
|
+
}
|
|
2457
|
+
if (this.store.hasRunFinalDelivery(this.config.name, record.chatId, record.provenance, false)) {
|
|
2458
|
+
return { state: "pending", reason: "exact_run_final_delivery_pending" };
|
|
2459
|
+
}
|
|
2460
|
+
let verdict;
|
|
2461
|
+
try {
|
|
2462
|
+
verdict = await this.openclawClient.inspectRunError(record.provenance);
|
|
2463
|
+
}
|
|
2464
|
+
catch {
|
|
2465
|
+
verdict = { state: "pending", reason: "inspection_unavailable" };
|
|
2466
|
+
}
|
|
2467
|
+
// A stop or final may have arrived during the read-only lookup.
|
|
2468
|
+
const latest = this.store.getErrorNotice(this.config.name, record.chatId, record.noticeId);
|
|
2469
|
+
if (latest?.provenance.userStop) {
|
|
2470
|
+
record.provenance.userStop = true;
|
|
2471
|
+
return { state: "stopped", reason: "explicit_bridge_stop" };
|
|
2472
|
+
}
|
|
2473
|
+
if (latest && (latest.provenance.source !== record.provenance.source || latest.provenance.state !== record.provenance.state || latest.provenance.stopReason !== record.provenance.stopReason)) {
|
|
2474
|
+
record.provenance = latest.provenance;
|
|
2475
|
+
record.checks = latest.checks;
|
|
2476
|
+
return { state: "pending", reason: "stronger_terminal_evidence_observed" };
|
|
2477
|
+
}
|
|
2478
|
+
if (this.store.hasRunFinalDelivery(this.config.name, record.chatId, record.provenance, true)) {
|
|
2479
|
+
return { state: "suppressed", reason: "exact_run_final_platform_delivery_confirmed" };
|
|
2480
|
+
}
|
|
2481
|
+
if (this.store.hasRunFinalDelivery(this.config.name, record.chatId, record.provenance, false)) {
|
|
2482
|
+
return { state: "pending", reason: "exact_run_final_delivery_pending" };
|
|
2483
|
+
}
|
|
2484
|
+
return verdict;
|
|
2485
|
+
}
|
|
2486
|
+
deferErrorNotice(record, verdict) {
|
|
2487
|
+
const delays = [15_000, 60_000, 120_000, 300_000, FOREGROUND_WAIT_MS];
|
|
2488
|
+
record.verdict = verdict;
|
|
2489
|
+
// Known-live task/delivery state is not an unknown diagnosis. Continue
|
|
2490
|
+
// low-frequency checks across the Gateway's 30-minute retry window, bounded
|
|
2491
|
+
// by the existing 24-hour observation horizon. Unknown evidence stays capped.
|
|
2492
|
+
if (verdict.reason === "matching_task_or_delivery_continuing")
|
|
2493
|
+
record.hasObservedContinuingTask = true;
|
|
2494
|
+
const continuing = record.hasObservedContinuingTask === true;
|
|
2495
|
+
const budget = continuing ? 300 : 6;
|
|
2496
|
+
record.status = record.checks >= budget || Date.now() - record.createdAt >= 24 * 60 * 60_000 ? "parked" : "pending";
|
|
2497
|
+
record.nextCheckAt = Date.now() + (continuing && record.checks >= 4 ? 300_000 : (delays[Math.max(0, record.checks - 1)] || FOREGROUND_WAIT_MS));
|
|
2498
|
+
this.store.saveErrorNotice(record);
|
|
2499
|
+
this.scheduleErrorNoticeCheck(record.chatId);
|
|
2500
|
+
}
|
|
2501
|
+
scheduleErrorNoticeCheck(chatId) {
|
|
2502
|
+
const previous = this.errorNoticeTimers.get(chatId);
|
|
2503
|
+
if (previous)
|
|
2504
|
+
clearTimeout(previous);
|
|
2505
|
+
this.errorNoticeTimers.delete(chatId);
|
|
2506
|
+
const next = this.store.listPendingErrorNotices(this.config.name, chatId)[0];
|
|
2507
|
+
if (!next)
|
|
2508
|
+
return;
|
|
2509
|
+
const timer = setTimeout(() => {
|
|
2510
|
+
this.errorNoticeTimers.delete(chatId);
|
|
2511
|
+
void this.reconcileErrorNotices(chatId).catch(err => console.warn(`[${this.config.name}] Error notice check unavailable:`, this.errorSummary(err)));
|
|
2512
|
+
}, Math.max(1000, next.nextCheckAt - Date.now()));
|
|
2513
|
+
timer.unref?.();
|
|
2514
|
+
this.errorNoticeTimers.set(chatId, timer);
|
|
2515
|
+
}
|
|
2516
|
+
/** Bounded read-only diagnosis; known live tasks retain low-frequency observation. */
|
|
2517
|
+
async reconcileErrorNotices(chatId) {
|
|
2518
|
+
const running = this.errorNoticeChecks.get(chatId);
|
|
2519
|
+
if (running)
|
|
2520
|
+
return running;
|
|
2521
|
+
const work = async () => {
|
|
2522
|
+
const timer = this.errorNoticeTimers.get(chatId);
|
|
2523
|
+
if (timer)
|
|
2524
|
+
clearTimeout(timer);
|
|
2525
|
+
this.errorNoticeTimers.delete(chatId);
|
|
2526
|
+
for (const snapshot of this.store.listPendingErrorNotices(this.config.name, chatId)) {
|
|
2527
|
+
// Earlier awaits may have observed a stop/stronger envelope for this row.
|
|
2528
|
+
const record = this.store.getErrorNotice(this.config.name, chatId, snapshot.noticeId);
|
|
2529
|
+
if (!record || record.status !== "pending")
|
|
2530
|
+
continue;
|
|
2531
|
+
if (record.nextCheckAt > Date.now())
|
|
2532
|
+
continue;
|
|
2533
|
+
if (record.checks >= (record.hasObservedContinuingTask || record.verdict?.reason === "matching_task_or_delivery_continuing" ? 300 : 6)) {
|
|
2534
|
+
// Normal exhausted checks are already parked. A still-pending row at
|
|
2535
|
+
// the cap can be a crash after budget reservation / before verdict or
|
|
2536
|
+
// outbox creation. Reserve one durable recovery probe, never per boot.
|
|
2537
|
+
if (record.recoveryProbeUsed) {
|
|
2538
|
+
this.deferErrorNotice(record, record.verdict || { state: "pending", reason: "budget_exhausted" });
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
record.recoveryProbeUsed = true;
|
|
2542
|
+
}
|
|
2543
|
+
record.checks++;
|
|
2544
|
+
// Reserve the budget before awaiting RPC, including crash/disconnect.
|
|
2545
|
+
this.store.saveErrorNotice(record);
|
|
2546
|
+
const verdict = await this.inspectErrorNotice(record);
|
|
2547
|
+
console.log(`[${this.config.name}] Error notice ${record.noticeId}: ${verdict.state}/${verdict.reason}`);
|
|
2548
|
+
if (verdict.state === "pending") {
|
|
2549
|
+
this.deferErrorNotice(record, verdict);
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
record.verdict = verdict;
|
|
2553
|
+
this.store.saveErrorNotice(record);
|
|
2554
|
+
if (verdict.state === "terminal" && verdict.terminalKey && verdict.text) {
|
|
2555
|
+
// Independent identity: never take the result's trigger/final key.
|
|
2556
|
+
await this.enqueueAndDispatchDelivery(chatId, "run_error", verdict.terminalKey, verdict.text, [], undefined, verdict.terminalKey, undefined, { sourceType: "run_error", runId: record.provenance.runId, error: record.provenance, terminalEvidence: verdict }, record.noticeId);
|
|
2557
|
+
// The send-time recheck may have parked or suppressed the observation.
|
|
2558
|
+
const refreshed = this.store.getErrorNotice(this.config.name, chatId, record.noticeId);
|
|
2559
|
+
if (refreshed?.verdict && ["pending", "suppressed", "stopped"].includes(refreshed.verdict.state))
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
record.status = verdict.state;
|
|
2563
|
+
this.store.saveErrorNotice(record);
|
|
2564
|
+
}
|
|
2565
|
+
this.scheduleErrorNoticeCheck(chatId);
|
|
2566
|
+
};
|
|
2567
|
+
const promise = work().finally(() => { this.errorNoticeChecks.delete(chatId); });
|
|
2568
|
+
this.errorNoticeChecks.set(chatId, promise);
|
|
2569
|
+
return promise;
|
|
2570
|
+
}
|
|
2411
2571
|
scheduleDeliveryRetry(chatId, attempt) {
|
|
2412
2572
|
if (this.deliveryRetryTimers.has(chatId))
|
|
2413
2573
|
return;
|
|
@@ -2454,7 +2614,32 @@ export class FeishuBot {
|
|
|
2454
2614
|
const finalStatus = needsStatus ? formatSessionFooter(await this.readSessionStatus(chatId)) : undefined;
|
|
2455
2615
|
if (needsStatus && !finalModel)
|
|
2456
2616
|
finalModel = this.config.model;
|
|
2457
|
-
if (
|
|
2617
|
+
if (item.sourceType === "run_error") {
|
|
2618
|
+
const meta = JSON.parse(item.deliveryMetaJson || "{}");
|
|
2619
|
+
const record = this.store.getErrorNotice(this.config.name, chatId, meta.errorNoticeId || "");
|
|
2620
|
+
const verdict = record ? await this.inspectErrorNotice(record) : { state: "pending", reason: "missing_persisted_provenance" };
|
|
2621
|
+
if (verdict.state !== "terminal" || verdict.terminalKey !== item.deliveryKey || verdict.text !== item.content) {
|
|
2622
|
+
this.store.withdrawErrorDelivery(item.id);
|
|
2623
|
+
if (record) {
|
|
2624
|
+
if (verdict.state === "pending" || verdict.state === "terminal")
|
|
2625
|
+
this.deferErrorNotice(record, { ...verdict, state: "pending" });
|
|
2626
|
+
else {
|
|
2627
|
+
record.status = verdict.state;
|
|
2628
|
+
record.verdict = verdict;
|
|
2629
|
+
this.store.saveErrorNotice(record);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
record.verdict = verdict;
|
|
2635
|
+
this.store.saveErrorNotice(record);
|
|
2636
|
+
}
|
|
2637
|
+
if (item.sourceType === "run_error") {
|
|
2638
|
+
// Exactly one platform operation per guarded attempt. A card/text
|
|
2639
|
+
// fallback must not bypass a fresh finality check.
|
|
2640
|
+
await this.sendErrorNotice(chatId, item.content);
|
|
2641
|
+
}
|
|
2642
|
+
else if (shouldReplyToSource) {
|
|
2458
2643
|
try {
|
|
2459
2644
|
await this.replyFinalMessage(replyTarget, item.content, finalModel, finalStatus);
|
|
2460
2645
|
}
|
|
@@ -2515,6 +2700,8 @@ export class FeishuBot {
|
|
|
2515
2700
|
return;
|
|
2516
2701
|
}
|
|
2517
2702
|
this.store.markDeliveryFailed(item.id);
|
|
2703
|
+
if (item.sourceType === "run_error")
|
|
2704
|
+
return;
|
|
2518
2705
|
const label = failureStage === "attachment" ? "附件发送失败" : "最终回复发送失败";
|
|
2519
2706
|
const errorText = `⚠️ ${label}(已重试 ${DELIVERY_MAX_ATTEMPTS} 次):${this.errorSummary(err)}`;
|
|
2520
2707
|
const replyTarget = item.replyToMessageId || replyToMessageId;
|
|
@@ -2602,6 +2789,24 @@ export class FeishuBot {
|
|
|
2602
2789
|
const bot = Array.from(FeishuBot.allBots.values()).find((candidate) => candidate.store === this.store && candidate.config.name === chairman && this.isBotAvailableInChat(candidate, chatId));
|
|
2603
2790
|
return bot ? this.asDiscussionParticipant(bot, chatId) : undefined;
|
|
2604
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
|
+
}
|
|
2605
2810
|
asDiscussionParticipant(bot, chatId) {
|
|
2606
2811
|
return {
|
|
2607
2812
|
name: bot.config.name,
|
|
@@ -2620,6 +2825,7 @@ export class FeishuBot {
|
|
|
2620
2825
|
}, {
|
|
2621
2826
|
botName: this.config.name,
|
|
2622
2827
|
model: this.config.model,
|
|
2828
|
+
thinkingLevel: await this.fetchThinkingLabel(sessionKey),
|
|
2623
2829
|
locale: this.isEn(chatId) ? "en" : "zh",
|
|
2624
2830
|
})
|
|
2625
2831
|
: undefined;
|
|
@@ -2719,6 +2925,7 @@ export class FeishuBot {
|
|
|
2719
2925
|
toolCalls: view.toolCalls,
|
|
2720
2926
|
elapsed: view.elapsed,
|
|
2721
2927
|
model: view.model,
|
|
2928
|
+
thinkingLevel: view.thinkingLevel,
|
|
2722
2929
|
locale: en ? "en" : "zh",
|
|
2723
2930
|
}, statusEmoji);
|
|
2724
2931
|
return {
|
|
@@ -2754,8 +2961,12 @@ export class FeishuBot {
|
|
|
2754
2961
|
// Keep the active model visible while the run is in progress or failed.
|
|
2755
2962
|
// The final reply also keeps model attribution so it remains self-contained.
|
|
2756
2963
|
const footerBits = [en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`];
|
|
2757
|
-
if (view.model?.trim())
|
|
2758
|
-
|
|
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
|
+
}
|
|
2759
2970
|
elements.push({ tag: "markdown", content: `<font color='grey'>${footerBits.join(" · ")}</font>` });
|
|
2760
2971
|
const template = view.state === "failed" ? "orange" : "blue";
|
|
2761
2972
|
return {
|
|
@@ -2842,7 +3053,9 @@ export class FeishuBot {
|
|
|
2842
3053
|
// Running: phase line + ticking elapsed footer.
|
|
2843
3054
|
const phaseLine = view.phase === "transcript-trim"
|
|
2844
3055
|
? (en ? "🧹 Semantic compaction unavailable — trimming transcript safely…" : "🧹 语义压缩不可用,正在安全裁剪转录…")
|
|
2845
|
-
:
|
|
3056
|
+
: view.phase === "trim"
|
|
3057
|
+
? (en ? "🧹 Trimming transcript tail (keeping the most recent events)…" : "🧹 正在裁剪转录尾部(保留最近的事件)…")
|
|
3058
|
+
: (en ? "🧹 Compacting session…" : "🧹 正在压缩 session…");
|
|
2846
3059
|
const footer = en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`;
|
|
2847
3060
|
return {
|
|
2848
3061
|
schema: "2.0",
|
|
@@ -3280,6 +3493,16 @@ export class FeishuBot {
|
|
|
3280
3493
|
/**
|
|
3281
3494
|
* Send a proactive message to a chat (not a reply).
|
|
3282
3495
|
*/
|
|
3496
|
+
async sendErrorNotice(chatId, text) {
|
|
3497
|
+
const res = await this.client.im.message.create({
|
|
3498
|
+
params: { receive_id_type: "chat_id" },
|
|
3499
|
+
data: { receive_id: chatId, msg_type: "text", content: JSON.stringify({ text }) },
|
|
3500
|
+
});
|
|
3501
|
+
const id = res?.data?.message_id || res?.message_id;
|
|
3502
|
+
if (!id)
|
|
3503
|
+
throw new Error("Error notice send returned no confirmed message id");
|
|
3504
|
+
return id;
|
|
3505
|
+
}
|
|
3283
3506
|
async sendFinalMessage(chatId, text, model, status) {
|
|
3284
3507
|
if (!model?.trim())
|
|
3285
3508
|
return this.sendMessage(chatId, text);
|
|
@@ -3609,16 +3832,37 @@ export class FeishuBot {
|
|
|
3609
3832
|
* Handle /compact command: compress session context.
|
|
3610
3833
|
*/
|
|
3611
3834
|
/**
|
|
3612
|
-
* Compact a session
|
|
3613
|
-
*
|
|
3614
|
-
* 2. If summarization cannot run, ask the same RPC to retain only a bounded
|
|
3615
|
-
* transcript tail via `maxLines`. This works with legacy JSONL and the
|
|
3616
|
-
* SQLite session store introduced in OpenClaw 2026.8.x, and preserves
|
|
3617
|
-
* 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.
|
|
3618
3837
|
*
|
|
3619
|
-
*
|
|
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.
|
|
3620
3844
|
*/
|
|
3621
|
-
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.
|
|
3622
3866
|
let nativeReason;
|
|
3623
3867
|
try {
|
|
3624
3868
|
const res = await this.openclawClient.compactSession(sessionKey);
|
|
@@ -3630,11 +3874,10 @@ export class FeishuBot {
|
|
|
3630
3874
|
nativeReason = err.message;
|
|
3631
3875
|
}
|
|
3632
3876
|
onPhase?.("transcript-trim");
|
|
3633
|
-
const maxLines = compactFallbackMaxLines();
|
|
3634
3877
|
try {
|
|
3635
|
-
const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines });
|
|
3878
|
+
const trimmed = await this.openclawClient.compactSession(sessionKey, { maxLines: trimMaxLines });
|
|
3636
3879
|
if (trimmed?.compacted === true) {
|
|
3637
|
-
const kept = typeof trimmed?.kept === "number" ? trimmed.kept :
|
|
3880
|
+
const kept = typeof trimmed?.kept === "number" ? trimmed.kept : trimMaxLines;
|
|
3638
3881
|
return {
|
|
3639
3882
|
compacted: true,
|
|
3640
3883
|
method: "transcript-trim",
|
|
@@ -3659,25 +3902,42 @@ export class FeishuBot {
|
|
|
3659
3902
|
};
|
|
3660
3903
|
}
|
|
3661
3904
|
}
|
|
3662
|
-
async handleCompactCommand(chatId, messageId) {
|
|
3905
|
+
async handleCompactCommand(chatId, messageId, args = "") {
|
|
3663
3906
|
const sessionKey = this.getSessionKey(chatId);
|
|
3664
3907
|
const en = this.isEn(chatId);
|
|
3665
|
-
//
|
|
3666
|
-
//
|
|
3667
|
-
//
|
|
3668
|
-
//
|
|
3669
|
-
//
|
|
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
|
+
}
|
|
3670
3928
|
const progress = new CompactProgressController({
|
|
3671
3929
|
create: (view) => this.sendOrdered(chatId, () => this.replyCompactCard(messageId, view, chatId)),
|
|
3672
3930
|
edit: (id, view) => this.sendOrdered(chatId, () => this.patchCompactCard(id, view, chatId)),
|
|
3673
3931
|
warn: (msg, err) => console.warn(`[${this.config.name}] ${msg}`, err instanceof Error ? err.message : err),
|
|
3674
3932
|
}, { locale: en ? "en" : "zh" });
|
|
3933
|
+
if (mode === "trim")
|
|
3934
|
+
progress.toTrim();
|
|
3675
3935
|
progress.start();
|
|
3676
3936
|
try {
|
|
3677
3937
|
const r = await this.compactWithFallback(sessionKey, (phase) => {
|
|
3678
3938
|
if (phase === "transcript-trim")
|
|
3679
3939
|
void progress.toTranscriptTrim().catch(() => { });
|
|
3680
|
-
});
|
|
3940
|
+
}, { mode, maxLines });
|
|
3681
3941
|
if (r.compacted) {
|
|
3682
3942
|
// If a card was shown, patch it in place; otherwise (fast finish, no card)
|
|
3683
3943
|
// fall back to a normal reply so the user still gets confirmation.
|
|
@@ -3840,9 +4100,6 @@ export class FeishuBot {
|
|
|
3840
4100
|
console.warn(`[${this.config.name}] Failed to fetch chat info:`, err.message);
|
|
3841
4101
|
}
|
|
3842
4102
|
}
|
|
3843
|
-
/**
|
|
3844
|
-
* Send a model-drift notification to the affected chat.
|
|
3845
|
-
*/
|
|
3846
4103
|
async hydrateInlineImageKeys(text, messageId) {
|
|
3847
4104
|
const imageKeyPattern = /\[Image: (img_[^\]\n]+)\]/g;
|
|
3848
4105
|
const replacements = [];
|
|
@@ -4070,24 +4327,4 @@ export class FeishuBot {
|
|
|
4070
4327
|
}
|
|
4071
4328
|
return this.cleanMentions(parts.join(" "));
|
|
4072
4329
|
}
|
|
4073
|
-
async notifyModelDrift(chatId, _sessionKey) {
|
|
4074
|
-
try {
|
|
4075
|
-
const chatInfo = this.store.getChatInfo(chatId);
|
|
4076
|
-
const chatLabel = chatInfo?.chatName || chatId.slice(-8);
|
|
4077
|
-
await this.client.im.message.create({
|
|
4078
|
-
params: { receive_id_type: "chat_id" },
|
|
4079
|
-
data: {
|
|
4080
|
-
receive_id: chatId,
|
|
4081
|
-
content: JSON.stringify({
|
|
4082
|
-
text: `⚠️ 模型漂移已自动纠正\n期望: ${this.config.model}\n已恢复`,
|
|
4083
|
-
}),
|
|
4084
|
-
msg_type: "text",
|
|
4085
|
-
},
|
|
4086
|
-
});
|
|
4087
|
-
console.log(`[${this.config.name}] Drift notification sent to ${chatLabel}`);
|
|
4088
|
-
}
|
|
4089
|
-
catch (err) {
|
|
4090
|
-
console.warn(`[${this.config.name}] Failed to notify drift:`, err.message);
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
4330
|
}
|
package/dist/live-status.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type LiveStatusView = {
|
|
|
16
16
|
elapsed: string;
|
|
17
17
|
/** Footer: model name, e.g. "phgeek-gw/claude-opus-4.8". */
|
|
18
18
|
model?: string;
|
|
19
|
+
/** Footer: current thinking level label, e.g. "high" / "adaptive". */
|
|
20
|
+
thinkingLevel?: string;
|
|
19
21
|
/** Total tool calls in the run (for the compact finished summary). */
|
|
20
22
|
toolCalls: number;
|
|
21
23
|
/** True when finished via noReply(): the model produced no user-visible reply. */
|
|
@@ -29,6 +31,7 @@ export type LiveStatusFinalMeta = {
|
|
|
29
31
|
toolCalls: number;
|
|
30
32
|
elapsed: string;
|
|
31
33
|
model?: string;
|
|
34
|
+
thinkingLevel?: string;
|
|
32
35
|
locale: "zh" | "en";
|
|
33
36
|
noReply?: boolean;
|
|
34
37
|
};
|
|
@@ -46,6 +49,8 @@ export type LiveStatusOptions = {
|
|
|
46
49
|
maxChars?: number;
|
|
47
50
|
/** Model name shown in the footer. */
|
|
48
51
|
model?: string;
|
|
52
|
+
/** Thinking level label shown after the model name in the footer. */
|
|
53
|
+
thinkingLevel?: string;
|
|
49
54
|
/** Number of recent activity lines kept in the content area. */
|
|
50
55
|
historySize?: number;
|
|
51
56
|
/** Footer/auto-refresh cadence in ms. */
|
package/dist/live-status.js
CHANGED
|
@@ -172,6 +172,7 @@ export class LiveStatusController {
|
|
|
172
172
|
toolCalls: this.toolCallCount,
|
|
173
173
|
elapsed: this.formatElapsed(),
|
|
174
174
|
model: this.opts.model,
|
|
175
|
+
thinkingLevel: this.opts.thinkingLevel,
|
|
175
176
|
locale: this.opts.locale === "en" ? "en" : "zh",
|
|
176
177
|
noReply,
|
|
177
178
|
};
|
|
@@ -403,6 +404,7 @@ export class LiveStatusController {
|
|
|
403
404
|
lines,
|
|
404
405
|
elapsed: this.formatElapsed(),
|
|
405
406
|
model: this.opts.model,
|
|
407
|
+
thinkingLevel: this.opts.thinkingLevel,
|
|
406
408
|
toolCalls: this.toolCallCount,
|
|
407
409
|
noReply: this.noReplyResult,
|
|
408
410
|
state: this.state,
|