openclaw-lark-multi-agent 1.8.1 → 1.8.2
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/feishu-bot.js +27 -2
- package/dist/openclaw-client.d.ts +20 -0
- package/dist/openclaw-client.js +78 -2
- package/package.json +1 -1
package/dist/feishu-bot.js
CHANGED
|
@@ -5,6 +5,7 @@ import { persistBotModel } from "./config.js";
|
|
|
5
5
|
import { getI18n, normalizeLocale } from "./i18n.js";
|
|
6
6
|
import { InactiveRunObservation } from "./openclaw-client.js";
|
|
7
7
|
import { SessionWaitPaused, isWaitTimeout, normalizeSessionRuntimeStatus, formatSessionFooter, FOREGROUND_WAIT_MS } from "./session-status.js";
|
|
8
|
+
import { SupersededObservation } from "./openclaw-client.js";
|
|
8
9
|
import { LiveStatusController } from "./live-status.js";
|
|
9
10
|
import { CompactProgressController } from "./compact-progress.js";
|
|
10
11
|
import { GoController } from "./go-controller.js";
|
|
@@ -1253,8 +1254,17 @@ export class FeishuBot {
|
|
|
1253
1254
|
this.admittedSteerAcks.set(chatId, (this.admittedSteerAcks.get(chatId) || []).filter((a) => a.insertedId !== insertedId));
|
|
1254
1255
|
console.log(`[${this.config.name}] steer fallback restored after failed run for ${chatId.slice(-8)}: "${cleanText.substring(0, 40)}..."`);
|
|
1255
1256
|
// Deferred: the current queue run is still active here; a fresh
|
|
1256
|
-
// drain must start after its finally releases the chat.
|
|
1257
|
-
|
|
1257
|
+
// drain must start after its finally releases the chat. A 0ms
|
|
1258
|
+
// timer is NOT enough — the owner's catch block still has
|
|
1259
|
+
// in-flight awaits (error delivery), so processQueue would join
|
|
1260
|
+
// the dying owner and return, stranding the restored trigger
|
|
1261
|
+
// until the next user message (2026-10-16 audit finding).
|
|
1262
|
+
const ownerNow = this.queueRuns.get(chatId);
|
|
1263
|
+
const drainRestored = () => { void this.processQueue(chatId).catch(() => { }); };
|
|
1264
|
+
if (ownerNow)
|
|
1265
|
+
void ownerNow.then(drainRestored, drainRestored).catch(() => { });
|
|
1266
|
+
else
|
|
1267
|
+
setTimeout(drainRestored, 0).unref?.();
|
|
1258
1268
|
});
|
|
1259
1269
|
this.runFailureObservers.set(chatId, observers);
|
|
1260
1270
|
const owner = this.queueRuns.get(chatId);
|
|
@@ -1493,6 +1503,13 @@ export class FeishuBot {
|
|
|
1493
1503
|
if (!owner)
|
|
1494
1504
|
return false;
|
|
1495
1505
|
console.warn(`[${this.config.name}] force-expiring zombie queue owner for ${chatId.slice(-8)}: ${reason}`);
|
|
1506
|
+
// Fence the stale batch's local observation BEFORE releasing custody: an
|
|
1507
|
+
// alive old poller would keep destructively consuming the replacement
|
|
1508
|
+
// batch's frames from the shared session bucket (2026-10-16 regression).
|
|
1509
|
+
try {
|
|
1510
|
+
this.openclawClient.revokeSessionCollectors(this.getSessionKey(chatId), `queue owner force-expired: ${reason}`);
|
|
1511
|
+
}
|
|
1512
|
+
catch { /* isolated */ }
|
|
1496
1513
|
this.queueRuns.delete(chatId);
|
|
1497
1514
|
this.fireRunFailureObservers(chatId);
|
|
1498
1515
|
this.runFailureObservers.delete(chatId);
|
|
@@ -1993,6 +2010,14 @@ export class FeishuBot {
|
|
|
1993
2010
|
}
|
|
1994
2011
|
}
|
|
1995
2012
|
catch (err) {
|
|
2013
|
+
if (err instanceof SupersededObservation) {
|
|
2014
|
+
// This batch's local observation was fenced by a newer batch (or a
|
|
2015
|
+
// custody revocation). The new owner handles delivery from here; end
|
|
2016
|
+
// quietly — no error notice, no replay, no Gateway abort. The old
|
|
2017
|
+
// run's eventual reply still flows through the proactive path.
|
|
2018
|
+
console.log(`[${this.config.name}] batch observation superseded for ${chatId.slice(-8)}; ending quietly`);
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
1996
2021
|
let pause = err instanceof SessionWaitPaused ? err : undefined;
|
|
1997
2022
|
if (!pause && isWaitTimeout(err instanceof Error ? err.message : String(err))) {
|
|
1998
2023
|
const state = await this.readSessionStatus(chatId);
|
|
@@ -21,6 +21,16 @@ export type ProgressEvent = {
|
|
|
21
21
|
export declare class InactiveRunObservation extends Error {
|
|
22
22
|
constructor();
|
|
23
23
|
}
|
|
24
|
+
/** The local collectReply observation was fenced because a newer batch took
|
|
25
|
+
* over this session's queue custody. Purely local: no Gateway run was stopped,
|
|
26
|
+
* and the superseded run's eventual reply still flows through the proactive
|
|
27
|
+
* delivery path. Introduced for the 2026-10-16 stale-collector regression:
|
|
28
|
+
* an alive old poller destructively consumed the replacement batch's frames
|
|
29
|
+
* from the shared session bucket, starving the new collector's idle clock and
|
|
30
|
+
* swallowing its final. */
|
|
31
|
+
export declare class SupersededObservation extends Error {
|
|
32
|
+
constructor(reason?: string);
|
|
33
|
+
}
|
|
24
34
|
export type ChatAttachment = {
|
|
25
35
|
type?: string;
|
|
26
36
|
mimeType?: string;
|
|
@@ -59,6 +69,11 @@ export declare class OpenClawClient {
|
|
|
59
69
|
private recentToolEventKeys;
|
|
60
70
|
private sessionMessageCallbacks;
|
|
61
71
|
private progressCallbacks;
|
|
72
|
+
/** Live collectReply fences per canonical session key. The most recent
|
|
73
|
+
* collector for a session owns the shared event bucket; a new batch (or a
|
|
74
|
+
* queue-custody revocation) fences stale ones synchronously so they cannot
|
|
75
|
+
* steal frames from the replacement (2026-10-16 regression). */
|
|
76
|
+
private sessionCollectorFences;
|
|
62
77
|
/** Per-session steered messages awaiting consumption confirmation. Each
|
|
63
78
|
* accepted message is retained independently so identical/overlapping text
|
|
64
79
|
* cannot overwrite another correlation. */
|
|
@@ -209,6 +224,11 @@ export declare class OpenClawClient {
|
|
|
209
224
|
*/
|
|
210
225
|
/** Stop claiming an exact run's final output after observation has paused. */
|
|
211
226
|
releasePausedWait(runId: string): void;
|
|
227
|
+
/** Synchronously fence every live collectReply observation for a session.
|
|
228
|
+
* Local only — never aborts a Gateway run. Returns the number superseded.
|
|
229
|
+
* Used when queue custody is revoked or a new chat.send takes over, so a
|
|
230
|
+
* stale poller cannot steal the replacement batch's frames (2026-10-16). */
|
|
231
|
+
revokeSessionCollectors(sessionKey: string, reason: string): number;
|
|
212
232
|
getSessionRuntimeStatus(sessionKey: string): Promise<SessionRuntimeStatus>;
|
|
213
233
|
private classifyWaitTimeout;
|
|
214
234
|
getSessionInfo(sessionKey: string): Promise<any>;
|
package/dist/openclaw-client.js
CHANGED
|
@@ -12,6 +12,19 @@ export class InactiveRunObservation extends Error {
|
|
|
12
12
|
this.name = "InactiveRunObservation";
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
/** The local collectReply observation was fenced because a newer batch took
|
|
16
|
+
* over this session's queue custody. Purely local: no Gateway run was stopped,
|
|
17
|
+
* and the superseded run's eventual reply still flows through the proactive
|
|
18
|
+
* delivery path. Introduced for the 2026-10-16 stale-collector regression:
|
|
19
|
+
* an alive old poller destructively consumed the replacement batch's frames
|
|
20
|
+
* from the shared session bucket, starving the new collector's idle clock and
|
|
21
|
+
* swallowing its final. */
|
|
22
|
+
export class SupersededObservation extends Error {
|
|
23
|
+
constructor(reason = "本地观察已被更新的批次取代") {
|
|
24
|
+
super(`${reason}(未停止网关运行)`);
|
|
25
|
+
this.name = "SupersededObservation";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
export const GATEWAY_PROTOCOL_MIN = 4;
|
|
16
29
|
export const GATEWAY_PROTOCOL_MAX = 4;
|
|
17
30
|
const REQUIRED_GATEWAY_METHODS = [
|
|
@@ -63,6 +76,11 @@ export class OpenClawClient {
|
|
|
63
76
|
recentToolEventKeys = new Map();
|
|
64
77
|
sessionMessageCallbacks = new Map();
|
|
65
78
|
progressCallbacks = new Map();
|
|
79
|
+
/** Live collectReply fences per canonical session key. The most recent
|
|
80
|
+
* collector for a session owns the shared event bucket; a new batch (or a
|
|
81
|
+
* queue-custody revocation) fences stale ones synchronously so they cannot
|
|
82
|
+
* steal frames from the replacement (2026-10-16 regression). */
|
|
83
|
+
sessionCollectorFences = new Map();
|
|
66
84
|
/** Per-session steered messages awaiting consumption confirmation. Each
|
|
67
85
|
* accepted message is retained independently so identical/overlapping text
|
|
68
86
|
* cannot overwrite another correlation. */
|
|
@@ -1479,6 +1497,34 @@ export class OpenClawClient {
|
|
|
1479
1497
|
if (observationKey && this.runStateChecks.get(observationKey) === checkAfterNoActiveRun)
|
|
1480
1498
|
this.runStateChecks.delete(observationKey);
|
|
1481
1499
|
};
|
|
1500
|
+
// Fence registration: a newer batch on this session must be able to end
|
|
1501
|
+
// THIS observation synchronously (local cleanup + superseded settlement)
|
|
1502
|
+
// so the shared bucket is no longer destructively consumed by a stale
|
|
1503
|
+
// poller. The Gateway run is never aborted by fencing.
|
|
1504
|
+
const fenceSuperseded = () => {
|
|
1505
|
+
if (observationClosed)
|
|
1506
|
+
return;
|
|
1507
|
+
cleanupObservation();
|
|
1508
|
+
reject(new SupersededObservation());
|
|
1509
|
+
};
|
|
1510
|
+
if (observationKey) {
|
|
1511
|
+
let fences = this.sessionCollectorFences.get(observationKey);
|
|
1512
|
+
if (!fences) {
|
|
1513
|
+
fences = new Set();
|
|
1514
|
+
this.sessionCollectorFences.set(observationKey, fences);
|
|
1515
|
+
}
|
|
1516
|
+
fences.add(fenceSuperseded);
|
|
1517
|
+
const unregisterFence = () => {
|
|
1518
|
+
const current = this.sessionCollectorFences.get(observationKey);
|
|
1519
|
+
if (current) {
|
|
1520
|
+
current.delete(fenceSuperseded);
|
|
1521
|
+
if (current.size === 0)
|
|
1522
|
+
this.sessionCollectorFences.delete(observationKey);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
const baseCleanup = cleanupObservation;
|
|
1526
|
+
cleanupObservation = () => { baseCleanup(); unregisterFence(); };
|
|
1527
|
+
}
|
|
1482
1528
|
}).finally(() => cleanupObservation());
|
|
1483
1529
|
}
|
|
1484
1530
|
/**
|
|
@@ -1787,6 +1833,25 @@ export class OpenClawClient {
|
|
|
1787
1833
|
this.ownedDeliveryRunTimers.delete(runId);
|
|
1788
1834
|
this.ownedDeliveryRuns.delete(runId);
|
|
1789
1835
|
}
|
|
1836
|
+
/** Synchronously fence every live collectReply observation for a session.
|
|
1837
|
+
* Local only — never aborts a Gateway run. Returns the number superseded.
|
|
1838
|
+
* Used when queue custody is revoked or a new chat.send takes over, so a
|
|
1839
|
+
* stale poller cannot steal the replacement batch's frames (2026-10-16). */
|
|
1840
|
+
revokeSessionCollectors(sessionKey, reason) {
|
|
1841
|
+
const key = this.canonicalSessionKey(sessionKey);
|
|
1842
|
+
const fences = this.sessionCollectorFences.get(key);
|
|
1843
|
+
if (!fences || fences.size === 0)
|
|
1844
|
+
return 0;
|
|
1845
|
+
const count = fences.size;
|
|
1846
|
+
for (const fence of [...fences]) {
|
|
1847
|
+
try {
|
|
1848
|
+
fence();
|
|
1849
|
+
}
|
|
1850
|
+
catch { /* each fence is isolated */ }
|
|
1851
|
+
}
|
|
1852
|
+
console.warn(`[OpenClaw] superseded ${count} stale collector(s) for ${key.slice(-24)}: ${reason}`);
|
|
1853
|
+
return count;
|
|
1854
|
+
}
|
|
1790
1855
|
async getSessionRuntimeStatus(sessionKey) {
|
|
1791
1856
|
let timer;
|
|
1792
1857
|
try {
|
|
@@ -2360,6 +2425,10 @@ export class OpenClawClient {
|
|
|
2360
2425
|
let result;
|
|
2361
2426
|
const sendStartedAt = Date.now();
|
|
2362
2427
|
try {
|
|
2428
|
+
// A stale collector from a previous batch must not steal this batch's
|
|
2429
|
+
// frames from the shared session bucket (2026-10-16 regression). Fence
|
|
2430
|
+
// any still-live local observation BEFORE issuing chat.send.
|
|
2431
|
+
this.revokeSessionCollectors(sk, "new chat.send supersedes stale observation");
|
|
2363
2432
|
await params.onSendAttempt?.();
|
|
2364
2433
|
const sendChat = () => this.rpc("chat.send", {
|
|
2365
2434
|
sessionKey: this.canonicalSessionKey(sk),
|
|
@@ -2407,8 +2476,15 @@ export class OpenClawClient {
|
|
|
2407
2476
|
// collectReply returns. Keep a short grace window so normal chat replies
|
|
2408
2477
|
// are not delivered twice via the proactive-message path. Cron/LMA runs
|
|
2409
2478
|
// are unaffected because they do not go through chatSend.
|
|
2410
|
-
|
|
2411
|
-
|
|
2479
|
+
// CAS: a superseded send's finally must not delete a newer send's
|
|
2480
|
+
// progress callback (the fence settles the old promise after the new
|
|
2481
|
+
// batch may already have registered its own callback).
|
|
2482
|
+
if (params.onProgress) {
|
|
2483
|
+
if (this.progressCallbacks.get(sk) === params.onProgress)
|
|
2484
|
+
this.progressCallbacks.delete(sk);
|
|
2485
|
+
if (this.progressCallbacks.get(fullSessionKey) === params.onProgress)
|
|
2486
|
+
this.progressCallbacks.delete(fullSessionKey);
|
|
2487
|
+
}
|
|
2412
2488
|
if (waitPaused)
|
|
2413
2489
|
this.releasePausedWait(ownedRunId);
|
|
2414
2490
|
else
|