openclaw-lark-multi-agent 1.8.2 → 1.8.4
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.d.ts +5 -0
- package/dist/feishu-bot.js +43 -6
- package/dist/openclaw-client.js +14 -5
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -50,6 +50,11 @@ export declare class FeishuBot {
|
|
|
50
50
|
* DONE when that batch completes successfully; cleared (unflipped) when the
|
|
51
51
|
* batch scope ends any other way. */
|
|
52
52
|
private admittedSteerAcks;
|
|
53
|
+
/** Identity token of the CURRENT queue owner per chat. Steer acks carry
|
|
54
|
+
* the token of the batch that admitted them; a completion path only flips
|
|
55
|
+
* acks of its OWN batch, so a superseded inner can never DONE a newer
|
|
56
|
+
* batch's steered inputs (2026-10-16 audit B9). */
|
|
57
|
+
private queueRunTokens;
|
|
53
58
|
/** Go / Go-Checker supervision controller (per-bot instance). */
|
|
54
59
|
private readonly goDeliveryReceipts;
|
|
55
60
|
private readonly goReceiptApis;
|
package/dist/feishu-bot.js
CHANGED
|
@@ -149,6 +149,11 @@ export class FeishuBot {
|
|
|
149
149
|
* DONE when that batch completes successfully; cleared (unflipped) when the
|
|
150
150
|
* batch scope ends any other way. */
|
|
151
151
|
admittedSteerAcks = new Map();
|
|
152
|
+
/** Identity token of the CURRENT queue owner per chat. Steer acks carry
|
|
153
|
+
* the token of the batch that admitted them; a completion path only flips
|
|
154
|
+
* acks of its OWN batch, so a superseded inner can never DONE a newer
|
|
155
|
+
* batch's steered inputs (2026-10-16 audit B9). */
|
|
156
|
+
queueRunTokens = new Map();
|
|
152
157
|
/** Go / Go-Checker supervision controller (per-bot instance). */
|
|
153
158
|
goDeliveryReceipts = new AsyncLocalStorage();
|
|
154
159
|
goReceiptApis = new WeakSet();
|
|
@@ -1227,7 +1232,7 @@ export class FeishuBot {
|
|
|
1227
1232
|
// never (2026.9.9 removed mirrors); either way the message belongs
|
|
1228
1233
|
// to this batch for the Get -> DONE flip.
|
|
1229
1234
|
const steerAckList = this.admittedSteerAcks.get(chatId) || [];
|
|
1230
|
-
steerAckList.push({ messageId, insertedId });
|
|
1235
|
+
steerAckList.push({ messageId, insertedId, batch: this.queueRunTokens.get(chatId) });
|
|
1231
1236
|
this.admittedSteerAcks.set(chatId, steerAckList);
|
|
1232
1237
|
}
|
|
1233
1238
|
if (insertedId > 0 && !consumedBeforeAck) {
|
|
@@ -1303,8 +1308,14 @@ export class FeishuBot {
|
|
|
1303
1308
|
// path and it will never settle). Force-expire it and process this input
|
|
1304
1309
|
// NOW instead of queueing behind a dead promise forever.
|
|
1305
1310
|
if (steer.status === "no_active_run" && this.queueRuns.has(chatId)) {
|
|
1311
|
+
const ownerAtCheck = this.queueRuns.get(chatId);
|
|
1306
1312
|
const live = await this.openclawClient.getSessionRuntimeStatus(sessionKeyForSteer).catch(() => null);
|
|
1307
|
-
|
|
1313
|
+
// B1 (2026-10-16 audit): require POSITIVE idle evidence. An RPC
|
|
1314
|
+
// failure normalizes to {status:"unknown", running:false} and must
|
|
1315
|
+
// never authorize expiring a legitimately running owner. The owner
|
|
1316
|
+
// identity is also CAS-checked across the status RPC: a newer batch
|
|
1317
|
+
// that took over meanwhile must not be expired by stale evidence.
|
|
1318
|
+
if (live && !live.running && live.status !== "unknown" && this.queueRuns.get(chatId) === ownerAtCheck) {
|
|
1308
1319
|
console.warn(`[${this.config.name}] gateway reports session idle while queue owner exists for ${chatId.slice(-8)} — expiring zombie owner (status=${live.status})`);
|
|
1309
1320
|
this.forceExpireQueueOwner(chatId, `gateway idle (steer=${steer.status}, session=${live.status})`);
|
|
1310
1321
|
// Fall through to the normal send path below; the durable trigger
|
|
@@ -1478,7 +1489,9 @@ export class FeishuBot {
|
|
|
1478
1489
|
console.log(`[${this.config.name}] processQueue already running for ${chatId.slice(-8)}, joining existing run`);
|
|
1479
1490
|
return existing;
|
|
1480
1491
|
}
|
|
1481
|
-
const
|
|
1492
|
+
const batchToken = {};
|
|
1493
|
+
this.queueRunTokens.set(chatId, batchToken);
|
|
1494
|
+
const run = this.processQueueInner(chatId, batchToken).finally(() => {
|
|
1482
1495
|
// Ownership token guard: only the CURRENT owner may release the slot. A
|
|
1483
1496
|
// zombie owner that settles after forceExpireQueueOwner must not delete
|
|
1484
1497
|
// the replacement owner's entry (that would open a concurrent-run window).
|
|
@@ -1486,6 +1499,7 @@ export class FeishuBot {
|
|
|
1486
1499
|
this.queueRuns.delete(chatId);
|
|
1487
1500
|
this.runFailureObservers.delete(chatId);
|
|
1488
1501
|
this.admittedSteerAcks.delete(chatId);
|
|
1502
|
+
this.queueRunTokens.delete(chatId);
|
|
1489
1503
|
}
|
|
1490
1504
|
});
|
|
1491
1505
|
this.queueRuns.set(chatId, run);
|
|
@@ -1514,9 +1528,10 @@ export class FeishuBot {
|
|
|
1514
1528
|
this.fireRunFailureObservers(chatId);
|
|
1515
1529
|
this.runFailureObservers.delete(chatId);
|
|
1516
1530
|
this.admittedSteerAcks.delete(chatId);
|
|
1531
|
+
this.queueRunTokens.delete(chatId);
|
|
1517
1532
|
return true;
|
|
1518
1533
|
}
|
|
1519
|
-
async processQueueInner(chatId) {
|
|
1534
|
+
async processQueueInner(chatId, batchToken) {
|
|
1520
1535
|
while (true) {
|
|
1521
1536
|
// A fresh batch starts: steered inputs admitted into the PREVIOUS batch
|
|
1522
1537
|
// completed with it (consumed or not); their failure observers are stale.
|
|
@@ -1998,9 +2013,15 @@ export class FeishuBot {
|
|
|
1998
2013
|
this.pendingAckMessages.set(chatId, remainingAcks);
|
|
1999
2014
|
// Steered messages admitted into THIS batch were consumed with it:
|
|
2000
2015
|
// flip their Get → DONE alongside the batch's own triggers.
|
|
2001
|
-
|
|
2016
|
+
// B9 (2026-10-16 audit): flip only THIS batch's admitted steer acks. A
|
|
2017
|
+
// superseded inner completing late must never DONE a newer batch's
|
|
2018
|
+
// steered inputs — foreign acks stay for their owning batch.
|
|
2019
|
+
const allSteers = this.admittedSteerAcks.get(chatId) || [];
|
|
2020
|
+
const admittedSteers = allSteers.filter((s) => s.batch === batchToken);
|
|
2021
|
+
if (allSteers.length > 0) {
|
|
2022
|
+
this.admittedSteerAcks.set(chatId, allSteers.filter((s) => s.batch !== batchToken));
|
|
2023
|
+
}
|
|
2002
2024
|
if (admittedSteers.length > 0) {
|
|
2003
|
-
this.admittedSteerAcks.delete(chatId);
|
|
2004
2025
|
for (const s of admittedSteers) {
|
|
2005
2026
|
await this.removeReaction(s.messageId, "Get").catch(() => { });
|
|
2006
2027
|
await this.addReaction(s.messageId, "DONE").catch(() => { });
|
|
@@ -3091,6 +3112,22 @@ export class FeishuBot {
|
|
|
3091
3112
|
else {
|
|
3092
3113
|
this.store.markDeliveryTextDelivered(item.id);
|
|
3093
3114
|
}
|
|
3115
|
+
// Proactive deliveries have no trigger-reply recording path, so
|
|
3116
|
+
// without this row other bots' context sync never sees them
|
|
3117
|
+
// (2026-10-16 blind spot: an audit reply delivered to Feishu but
|
|
3118
|
+
// invisible to every other group member). The deterministic id
|
|
3119
|
+
// keyed by deliveryKey keeps crash-retries idempotent; the unique
|
|
3120
|
+
// constraint turns a replay into a no-op.
|
|
3121
|
+
if (String(item.deliveryKey || "").startsWith("proactive:")) {
|
|
3122
|
+
this.store.insert({
|
|
3123
|
+
chatId,
|
|
3124
|
+
messageId: `self-${this.config.name}-proactive-${item.deliveryKey}`,
|
|
3125
|
+
senderType: "bot",
|
|
3126
|
+
senderName: this.config.name,
|
|
3127
|
+
content: item.content,
|
|
3128
|
+
timestamp: Date.now(),
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3094
3131
|
}
|
|
3095
3132
|
// Attachments are independent from status-card cleanup. An unpatchable
|
|
3096
3133
|
// or deleted status card must never block otherwise-sendable files.
|
package/dist/openclaw-client.js
CHANGED
|
@@ -870,9 +870,14 @@ export class OpenClawClient {
|
|
|
870
870
|
return;
|
|
871
871
|
if (snapshot.status === "ok" && snapshot.yielded !== true) {
|
|
872
872
|
const terminal = snapshot.terminalReply;
|
|
873
|
+
// B6 (2026-10-16 audit): disposition "empty" is an authoritative
|
|
874
|
+
// no-visible-text terminal — never recycle an interim draft as the
|
|
875
|
+
// final answer. Only genuinely unspecified metadata may fall back.
|
|
873
876
|
const visible = terminal?.disposition === "visible" && typeof terminal.text === "string"
|
|
874
|
-
? terminal.text
|
|
875
|
-
:
|
|
877
|
+
? terminal.text
|
|
878
|
+
: terminal?.disposition === "silent" || terminal?.disposition === "empty"
|
|
879
|
+
? "NO_REPLY"
|
|
880
|
+
: this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
|
|
876
881
|
resolve(visible || "NO_REPLY");
|
|
877
882
|
}
|
|
878
883
|
else if (snapshot.status === "error" || snapshot.status === "timeout") {
|
|
@@ -1374,7 +1379,9 @@ export class OpenClawClient {
|
|
|
1374
1379
|
return;
|
|
1375
1380
|
}
|
|
1376
1381
|
if (isWaitTimeout(errText)) {
|
|
1377
|
-
|
|
1382
|
+
// B5 (2026-10-16 audit): already dequeued by the unified splice
|
|
1383
|
+
// above; a second splice here deleted the NEXT buffered frame
|
|
1384
|
+
// (a real chatFinal following a timeout error was swallowed).
|
|
1378
1385
|
if (!decidingTimeout) {
|
|
1379
1386
|
decidingTimeout = true;
|
|
1380
1387
|
void this.classifyWaitTimeout(errText, targetSessionKey || sessionKey).then(handleTimeoutResult);
|
|
@@ -1428,11 +1435,13 @@ export class OpenClawClient {
|
|
|
1428
1435
|
if (this.isRecoverableAgentError(errText)) {
|
|
1429
1436
|
pendingRuntimeFailureText = `⚠️ Agent 未正常完成\n原因: ${safeErrorSummary(errText)}`;
|
|
1430
1437
|
console.warn(`[OpenClaw] recoverable chat error for runId=${evRunId || runId}; deferring: ${safeErrorSummary(errText)}`);
|
|
1431
|
-
|
|
1438
|
+
// B5: already dequeued above — no second splice.
|
|
1432
1439
|
continue;
|
|
1433
1440
|
}
|
|
1434
1441
|
if (isWaitTimeout(errText)) {
|
|
1435
|
-
|
|
1442
|
+
// B5 (2026-10-16 audit): already dequeued by the unified splice
|
|
1443
|
+
// above; a second splice here deleted the NEXT buffered frame
|
|
1444
|
+
// (a real chatFinal following a timeout error was swallowed).
|
|
1436
1445
|
if (!decidingTimeout) {
|
|
1437
1446
|
decidingTimeout = true;
|
|
1438
1447
|
void this.classifyWaitTimeout(errText, targetSessionKey || sessionKey).then(handleTimeoutResult);
|