openclaw-lark-multi-agent 1.8.20 → 1.8.21
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/config.d.ts +15 -1
- package/dist/config.js +14 -2
- package/dist/feishu-bot.d.ts +37 -7
- package/dist/feishu-bot.js +227 -81
- package/dist/go-controller.d.ts +2 -0
- package/dist/go-controller.js +24 -8
- package/dist/message-store.d.ts +39 -1
- package/dist/message-store.js +81 -4
- package/dist/openclaw-client.d.ts +0 -4
- package/dist/openclaw-client.js +25 -12
- package/package.json +2 -2
package/dist/config.d.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { type Locale } from "./i18n.js";
|
|
2
|
+
/** Profile used by a bot that does not name one.
|
|
3
|
+
*
|
|
4
|
+
* Keeps every pre-profile configuration on the historical default. */
|
|
5
|
+
export declare const DEFAULT_BOT_PROFILE = "main";
|
|
2
6
|
export interface BotConfig {
|
|
3
7
|
name: string;
|
|
4
8
|
appId: string;
|
|
5
9
|
appSecret: string;
|
|
6
|
-
|
|
10
|
+
/** Model routed for this bot. Optional: when absent or blank the bot
|
|
11
|
+
* follows the model of its OpenClaw profile instead of overriding it. */
|
|
12
|
+
model?: string;
|
|
7
13
|
locale?: Locale;
|
|
8
14
|
/** Optional per-bot thinking level (informational fallback for footers). */
|
|
9
15
|
thinking?: string;
|
|
16
|
+
/** OpenClaw agent/profile that owns this bot's sessions.
|
|
17
|
+
*
|
|
18
|
+
* Optional for backward compatibility: a bot that does not name a profile
|
|
19
|
+
* keeps the historical `main` profile, so existing configs behave exactly
|
|
20
|
+
* as before. When set, the bot's sessions are addressed under that
|
|
21
|
+
* profile, giving it that profile's own workspace, memory and identity.
|
|
22
|
+
* Defaults to "main" when absent or blank. */
|
|
23
|
+
profile?: string;
|
|
10
24
|
}
|
|
11
25
|
export interface OpenClawConfig {
|
|
12
26
|
baseUrl: string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { normalizeLocale } from "./i18n.js";
|
|
4
|
+
/** Profile used by a bot that does not name one.
|
|
5
|
+
*
|
|
6
|
+
* Keeps every pre-profile configuration on the historical default. */
|
|
7
|
+
export const DEFAULT_BOT_PROFILE = "main";
|
|
4
8
|
export function loadConfig(path) {
|
|
5
9
|
const configPath = path || resolve(process.cwd(), "config.json");
|
|
6
10
|
const raw = readFileSync(configPath, "utf-8");
|
|
@@ -12,13 +16,21 @@ export function loadConfig(path) {
|
|
|
12
16
|
throw new Error("No bots configured");
|
|
13
17
|
}
|
|
14
18
|
for (const bot of config.bots) {
|
|
15
|
-
if (!bot.appId || !bot.appSecret
|
|
16
|
-
throw new Error(`Bot "${bot.name}" missing appId
|
|
19
|
+
if (!bot.appId || !bot.appSecret) {
|
|
20
|
+
throw new Error(`Bot "${bot.name}" missing appId or appSecret`);
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
config.locale = normalizeLocale(config.locale);
|
|
20
24
|
for (const bot of config.bots) {
|
|
21
25
|
bot.locale = normalizeLocale(bot.locale || config.locale);
|
|
26
|
+
// A bot without a profile keeps the historical `main` profile. Older
|
|
27
|
+
// configs stay byte-identical in behaviour; a profile is only a routing
|
|
28
|
+
// choice, never a required field.
|
|
29
|
+
const profile = String(bot.profile == null ? "" : bot.profile).trim();
|
|
30
|
+
bot.profile = profile || DEFAULT_BOT_PROFILE;
|
|
31
|
+
// An empty model means "follow the profile's own model".
|
|
32
|
+
const model = String(bot.model == null ? "" : bot.model).trim();
|
|
33
|
+
bot.model = model || undefined;
|
|
22
34
|
}
|
|
23
35
|
// Validate uniqueness
|
|
24
36
|
const names = config.bots.map((b) => b.name);
|
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -93,8 +93,6 @@ export declare class FeishuBot {
|
|
|
93
93
|
private deliveryRetryTimers;
|
|
94
94
|
/** Last time a real assistant-visible reply was successfully handed to the delivery pipeline. */
|
|
95
95
|
private lastRealDeliveryAt;
|
|
96
|
-
/** Per-chat: whether we've already sent the high-context alert this cycle. */
|
|
97
|
-
private contextAlerted;
|
|
98
96
|
/** Active pre-admission target for the current chat operation. */
|
|
99
97
|
private activeDeliveryTargets;
|
|
100
98
|
/** Stable targets for concurrent runs in one chat. A steer admission can
|
|
@@ -120,7 +118,21 @@ export declare class FeishuBot {
|
|
|
120
118
|
* Get the session key for a specific chat.
|
|
121
119
|
* Format: lma-<botname>-<chatId>
|
|
122
120
|
*/
|
|
121
|
+
/** OpenClaw profile that owns this bot's sessions.
|
|
122
|
+
*
|
|
123
|
+
* Defaults to "main" so a configuration written before profiles existed
|
|
124
|
+
* keeps its historical behaviour exactly. */
|
|
125
|
+
private get profile();
|
|
123
126
|
getSessionKey(chatId: string): string;
|
|
127
|
+
/** Physical session key for this bot: the logical key addressed under the
|
|
128
|
+
* bot's OpenClaw profile.
|
|
129
|
+
*
|
|
130
|
+
* The LOGICAL key (getSessionKey) stays profile-free so durable records keep
|
|
131
|
+
* their historical shape and never need a migration. Every key that is
|
|
132
|
+
* actually handed to the Gateway goes through this method, so a bot bound to
|
|
133
|
+
* a non-default profile owns its sessions under that profile (its own
|
|
134
|
+
* workspace, memory and identity) while the stored records stay compatible. */
|
|
135
|
+
physicalSessionKey(chatId: string): string;
|
|
124
136
|
private chatLocale;
|
|
125
137
|
private isEn;
|
|
126
138
|
private lmaBridgePolicy;
|
|
@@ -164,6 +176,14 @@ export declare class FeishuBot {
|
|
|
164
176
|
private handleResolvedMessage;
|
|
165
177
|
/** Separate guarded-admission proof. Aggregate busy never becomes idle. */
|
|
166
178
|
private observedFollowupGuard;
|
|
179
|
+
/** How long an unconfirmed input may stay silent before the user is told.
|
|
180
|
+
*
|
|
181
|
+
* A fixed 60s window misfired on every slow session: measured reply latency
|
|
182
|
+
* in a busy group was p50 3.8min / p90 20.2min / max 135min, so 85% of real
|
|
183
|
+
* replies exceeded the old threshold and every one of them produced a false
|
|
184
|
+
* "not confirmed" notice. Scale the window with the session's own observed
|
|
185
|
+
* latency so a healthy slow run is never reported as stuck. */
|
|
186
|
+
private unconfirmedGraceMs;
|
|
167
187
|
/** A positive refusal must be durable AND explained to the sender. */
|
|
168
188
|
private retainChangedFollowup;
|
|
169
189
|
private explainUnconfirmedFollowup;
|
|
@@ -266,11 +286,15 @@ export declare class FeishuBot {
|
|
|
266
286
|
* or 0 if it cannot be determined. Same basis as /status. */
|
|
267
287
|
private getContextUsagePercent;
|
|
268
288
|
/**
|
|
269
|
-
* After a
|
|
270
|
-
* threshold (default 80%), so they can compact before
|
|
271
|
-
* timeout/lock/overflow failure modes.
|
|
272
|
-
*
|
|
273
|
-
*
|
|
289
|
+
* After a message completes, alert the user when context usage is at or above
|
|
290
|
+
* the threshold (default 80%), so they can compact before the session bloats
|
|
291
|
+
* into the timeout/lock/overflow failure modes.
|
|
292
|
+
*
|
|
293
|
+
* The check runs after EVERY completed message and re-notifies on every message
|
|
294
|
+
* while usage stays above the threshold. There is deliberately no per-chat
|
|
295
|
+
* "already warned" latch: with one, a single failed send (or a stale latch) would
|
|
296
|
+
* silently suppress every later warning, and the user would stop being told to
|
|
297
|
+
* compact while their context keeps growing.
|
|
274
298
|
*/
|
|
275
299
|
private maybeAlertHighContext;
|
|
276
300
|
/** True when the agent's reply is exactly a completion phrase (zh or en,
|
|
@@ -486,6 +510,12 @@ export declare class FeishuBot {
|
|
|
486
510
|
* runs for the session, clears the busy lock and every pending trigger, and
|
|
487
511
|
* resets stuck reactions so new messages are processed normally again.
|
|
488
512
|
*/
|
|
513
|
+
/** Release every peer bot's in-memory scheduling state for one chat.
|
|
514
|
+
* Each bot instance keeps its own queueRuns / queueOwners / busyChats /
|
|
515
|
+
* pendingAckMessages maps; a durable row cleanup cannot reach those. A
|
|
516
|
+
* group-scoped /stop must retire them too, otherwise the peer's queue owner
|
|
517
|
+
* survives and its next message is treated as "busy" forever. */
|
|
518
|
+
private releasePeerBotsForChat;
|
|
489
519
|
private handleStopCommand;
|
|
490
520
|
/**
|
|
491
521
|
* Fetch chat info (name, type, members) via Feishu API and cache in SQLite.
|
package/dist/feishu-bot.js
CHANGED
|
@@ -32,7 +32,21 @@ export class SendDeadlineError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
const nonemptyReceipt = (value) => typeof value === 'string' && !!value.trim();
|
|
35
|
-
|
|
35
|
+
/** Resolve the PHYSICAL session key for a logical LMA key.
|
|
36
|
+
*
|
|
37
|
+
* A bot may be bound to a specific OpenClaw profile. The profile selects the
|
|
38
|
+
* agent that owns the session, so the physical key carries it as an
|
|
39
|
+
* `agent:<profile>:` prefix. A key that already carries an explicit
|
|
40
|
+
* `agent:<id>:` prefix is left untouched.
|
|
41
|
+
*
|
|
42
|
+
* With the default profile this is byte-identical to the historical
|
|
43
|
+
* `agent:main:<key>`, so existing durable records keep matching. */
|
|
44
|
+
const canonicalPhysicalSessionKey = (key, profile) => {
|
|
45
|
+
if (/^agent:[^:]+:/.test(key))
|
|
46
|
+
return key;
|
|
47
|
+
const agent = String(profile || "main").trim() || "main";
|
|
48
|
+
return `agent:${agent}:${key}`;
|
|
49
|
+
};
|
|
36
50
|
const require = createRequire(import.meta.url);
|
|
37
51
|
const LMA_VERSION = require("../package.json").version;
|
|
38
52
|
const MAX_BOT_STREAK = 10;
|
|
@@ -329,8 +343,6 @@ export class FeishuBot {
|
|
|
329
343
|
deliveryRetryTimers = new Map();
|
|
330
344
|
/** Last time a real assistant-visible reply was successfully handed to the delivery pipeline. */
|
|
331
345
|
lastRealDeliveryAt = new Map();
|
|
332
|
-
/** Per-chat: whether we've already sent the high-context alert this cycle. */
|
|
333
|
-
contextAlerted = new Map();
|
|
334
346
|
/** Active pre-admission target for the current chat operation. */
|
|
335
347
|
activeDeliveryTargets = new Map();
|
|
336
348
|
/** Stable targets for concurrent runs in one chat. A steer admission can
|
|
@@ -387,10 +399,11 @@ export class FeishuBot {
|
|
|
387
399
|
// Go / Go-Checker supervision (design: lma-go-checker-design-20260917).
|
|
388
400
|
this.goController = new GoController({
|
|
389
401
|
botName: config.name,
|
|
390
|
-
fallbackModel: config.model,
|
|
402
|
+
fallbackModel: config.model || "",
|
|
403
|
+
profile: this.profile,
|
|
391
404
|
store,
|
|
392
405
|
client: openclawClient,
|
|
393
|
-
executorSessionKeyFor: (chatId) => this.
|
|
406
|
+
executorSessionKeyFor: (chatId) => this.physicalSessionKey(chatId),
|
|
394
407
|
notify: (chatId, text) => this.sendOrdered(chatId, () => this.sendMessage(chatId, text)),
|
|
395
408
|
reply: (messageId, text) => this.replyMessage(messageId, text),
|
|
396
409
|
deliverExecutorResult: (chatId, result, goId) => this.deliverGoExecutorResult(chatId, result, goId),
|
|
@@ -405,7 +418,7 @@ export class FeishuBot {
|
|
|
405
418
|
if (typeof exactKey !== "string" || typeof sessionId !== "string" || typeof runId !== "string")
|
|
406
419
|
return;
|
|
407
420
|
for (const job of this.store.listActiveGoJobs(this.config.name)) {
|
|
408
|
-
if (exactKey !== job.executorSessionKey && exactKey !==
|
|
421
|
+
if (exactKey !== job.executorSessionKey && exactKey !== canonicalPhysicalSessionKey(job.executorSessionKey, this.profile))
|
|
409
422
|
continue;
|
|
410
423
|
if (sessionId !== job.executorSessionId)
|
|
411
424
|
continue;
|
|
@@ -469,9 +482,27 @@ export class FeishuBot {
|
|
|
469
482
|
* Get the session key for a specific chat.
|
|
470
483
|
* Format: lma-<botname>-<chatId>
|
|
471
484
|
*/
|
|
485
|
+
/** OpenClaw profile that owns this bot's sessions.
|
|
486
|
+
*
|
|
487
|
+
* Defaults to "main" so a configuration written before profiles existed
|
|
488
|
+
* keeps its historical behaviour exactly. */
|
|
489
|
+
get profile() {
|
|
490
|
+
return String(this.config.profile || "main").trim() || "main";
|
|
491
|
+
}
|
|
472
492
|
getSessionKey(chatId) {
|
|
473
493
|
return `lma-${this.config.name.toLowerCase()}-${chatId}`;
|
|
474
494
|
}
|
|
495
|
+
/** Physical session key for this bot: the logical key addressed under the
|
|
496
|
+
* bot's OpenClaw profile.
|
|
497
|
+
*
|
|
498
|
+
* The LOGICAL key (getSessionKey) stays profile-free so durable records keep
|
|
499
|
+
* their historical shape and never need a migration. Every key that is
|
|
500
|
+
* actually handed to the Gateway goes through this method, so a bot bound to
|
|
501
|
+
* a non-default profile owns its sessions under that profile (its own
|
|
502
|
+
* workspace, memory and identity) while the stored records stay compatible. */
|
|
503
|
+
physicalSessionKey(chatId) {
|
|
504
|
+
return canonicalPhysicalSessionKey(this.getSessionKey(chatId), this.profile);
|
|
505
|
+
}
|
|
475
506
|
chatLocale(chatId) {
|
|
476
507
|
return normalizeLocale(chatId ? this.store.getChatLocale(chatId) || this.locale : this.locale);
|
|
477
508
|
}
|
|
@@ -495,7 +526,11 @@ export class FeishuBot {
|
|
|
495
526
|
* Lazy: only creates on first message in that chat.
|
|
496
527
|
*/
|
|
497
528
|
async ensureSession(chatId, setupFence) {
|
|
498
|
-
|
|
529
|
+
// The cache is keyed by the LOGICAL key so it stays profile-free and
|
|
530
|
+
// consistent with the rest of the durable bookkeeping; the Gateway is
|
|
531
|
+
// always addressed with the PHYSICAL key (profile included).
|
|
532
|
+
const logicalKey = this.getSessionKey(chatId);
|
|
533
|
+
const sessionKey = this.physicalSessionKey(chatId);
|
|
499
534
|
await this.assertIdentityEffect();
|
|
500
535
|
const held = this.identityEffects?.getStore();
|
|
501
536
|
const expectedSessionId = held?.item.target.sessionId || setupFence?.sessionId;
|
|
@@ -506,9 +541,12 @@ export class FeishuBot {
|
|
|
506
541
|
if (info?.session?.sessionId !== setupFence.sessionId || info.session.archived)
|
|
507
542
|
throw new SupersededObservation('retained setup physical target changed');
|
|
508
543
|
}
|
|
509
|
-
if (this.initializedSessions.has(
|
|
544
|
+
if (this.initializedSessions.has(logicalKey)) {
|
|
510
545
|
// Already initialized this process lifetime, just ensure model
|
|
511
|
-
|
|
546
|
+
// No configured model means the bot follows its profile's own model.
|
|
547
|
+
const corrected = this.config.model
|
|
548
|
+
? (expectedSessionId ? await this.openclawClient.ensureModel(sessionKey, this.config.model, { sessionId: expectedSessionId, guard: setupGuard }) : await this.openclawClient.ensureModel(sessionKey, this.config.model))
|
|
549
|
+
: false;
|
|
512
550
|
setupGuard();
|
|
513
551
|
if (corrected) {
|
|
514
552
|
console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
|
|
@@ -524,7 +562,9 @@ export class FeishuBot {
|
|
|
524
562
|
if (existing?.session) {
|
|
525
563
|
// Session exists — preserve it, only ensure model is correct
|
|
526
564
|
console.log(`[${this.config.name}] Session exists: ${sessionKey} (tokens: ${existing.session.totalTokens || 0})`);
|
|
527
|
-
const corrected =
|
|
565
|
+
const corrected = this.config.model
|
|
566
|
+
? (expectedSessionId ? await this.openclawClient.ensureModel(sessionKey, this.config.model, { sessionId: expectedSessionId, guard: setupGuard }) : await this.openclawClient.ensureModel(sessionKey, this.config.model))
|
|
567
|
+
: false;
|
|
528
568
|
setupGuard();
|
|
529
569
|
if (corrected) {
|
|
530
570
|
console.log(`[${this.config.name}] Model auto-corrected to ${this.config.model}`);
|
|
@@ -534,11 +574,13 @@ export class FeishuBot {
|
|
|
534
574
|
// Session doesn't exist — create new
|
|
535
575
|
await this.openclawClient.createSession({
|
|
536
576
|
key: sessionKey,
|
|
537
|
-
model: this.config.model,
|
|
577
|
+
model: this.config.model || "",
|
|
538
578
|
label: `LMA: ${this.config.name} [${chatId.slice(-8)}]`,
|
|
539
579
|
});
|
|
540
|
-
//
|
|
541
|
-
|
|
580
|
+
// Only patch when a model is configured; otherwise the profile's own
|
|
581
|
+
// model must be left untouched.
|
|
582
|
+
if (this.config.model)
|
|
583
|
+
await this.openclawClient.patchSession({ key: sessionKey, model: this.config.model });
|
|
542
584
|
await this.injectBridgePolicy(sessionKey);
|
|
543
585
|
console.log(`[${this.config.name}] Session created: ${sessionKey} (model: ${this.config.model})`);
|
|
544
586
|
}
|
|
@@ -550,7 +592,7 @@ export class FeishuBot {
|
|
|
550
592
|
throw err;
|
|
551
593
|
}
|
|
552
594
|
setupGuard();
|
|
553
|
-
this.initializedSessions.add(
|
|
595
|
+
this.initializedSessions.add(logicalKey);
|
|
554
596
|
// Subscribe to session events (proactive messages + tool calls)
|
|
555
597
|
this.openclawClient.setVerboseTranscriptDelivery(sessionKey, this.store.getBotVerbose(this.config.name, chatId));
|
|
556
598
|
await this.openclawClient.subscribeSession(sessionKey, async (text, meta) => {
|
|
@@ -604,7 +646,7 @@ export class FeishuBot {
|
|
|
604
646
|
console.error(`[${this.config.name}] Failed to deliver proactive msg:`, err.message);
|
|
605
647
|
}
|
|
606
648
|
}, async (meta) => {
|
|
607
|
-
if (!meta.runId || this.store.isStoppedObservation(
|
|
649
|
+
if (!meta.runId || this.store.isStoppedObservation(canonicalPhysicalSessionKey(sessionKey.replace(/^agent:[^:]+:/, ''), this.profile), meta.runId))
|
|
608
650
|
return;
|
|
609
651
|
if (this.store.hasUnconfirmedRunDelivery(this.config.name, chatId, meta.runId))
|
|
610
652
|
return;
|
|
@@ -660,7 +702,7 @@ export class FeishuBot {
|
|
|
660
702
|
await this.wsClient.start({ eventDispatcher: this.eventDispatcher });
|
|
661
703
|
if (this.identityClosed)
|
|
662
704
|
return;
|
|
663
|
-
console.log(`[${this.config.name}] Bot started (model: ${this.config.model}, open_id: ${this.botOpenId || "unknown"})`);
|
|
705
|
+
console.log(`[${this.config.name}] Bot started (model: ${this.config.model || `profile ${this.profile}`}, open_id: ${this.botOpenId || "unknown"})`);
|
|
664
706
|
// Drain any unsynced messages left from previous run
|
|
665
707
|
this.drainOnStartup();
|
|
666
708
|
void this.drainIdentityIngress().catch(err => console.warn(`[${this.config.name}] Identity ingress recovery failed:`, err.message));
|
|
@@ -693,14 +735,14 @@ export class FeishuBot {
|
|
|
693
735
|
});
|
|
694
736
|
}
|
|
695
737
|
captureIdentityTarget(messageId, chatId) {
|
|
696
|
-
const key = this.
|
|
738
|
+
const key = this.physicalSessionKey(chatId);
|
|
697
739
|
const work = (async () => {
|
|
698
740
|
const info = await this.identitySessionInfo(key);
|
|
699
741
|
if (this.identityClosed)
|
|
700
742
|
return;
|
|
701
743
|
const session = info?.session;
|
|
702
744
|
const target = typeof session?.sessionId === 'string' && session.sessionId.trim() && session.sessionId.length <= 256 && session.archived !== true
|
|
703
|
-
&& (!session.key || typeof session.key === 'string' && canonicalPhysicalSessionKey(session.key) === canonicalPhysicalSessionKey(key))
|
|
745
|
+
&& (!session.key || typeof session.key === 'string' && canonicalPhysicalSessionKey(session.key, this.profile) === canonicalPhysicalSessionKey(key, this.profile))
|
|
704
746
|
? { state: 'bound', sessionKey: key, sessionId: session.sessionId } : { state: 'unbound', sessionKey: key };
|
|
705
747
|
if (target.state === 'unbound')
|
|
706
748
|
console.warn(`[${this.config.name}] Identity input ${messageId} has no proven original physical session; retained without retargeting`);
|
|
@@ -880,7 +922,12 @@ export class FeishuBot {
|
|
|
880
922
|
if (this.identityTargetRepairs.has(item.messageId))
|
|
881
923
|
return;
|
|
882
924
|
item.target = this.store.getIdentityTarget(this.config.name, this.config.appId, item.messageId);
|
|
883
|
-
|
|
925
|
+
// An identity target may hold the physical key (current records) or the
|
|
926
|
+
// logical key (records written before profiles existed). Both denote the
|
|
927
|
+
// SAME session, so both are accepted here; comparing against only one
|
|
928
|
+
// form silently skips identity recovery and the message is never sent.
|
|
929
|
+
if (item.target.state !== 'bound' || !item.target.sessionId ||
|
|
930
|
+
(item.target.sessionKey !== this.physicalSessionKey(item.chatId) && item.target.sessionKey !== this.getSessionKey(item.chatId)))
|
|
884
931
|
continue;
|
|
885
932
|
const info = await this.identitySessionInfo(item.target.sessionKey);
|
|
886
933
|
if (this.identityClosed)
|
|
@@ -1462,7 +1509,7 @@ export class FeishuBot {
|
|
|
1462
1509
|
const isOn = this.store.getBotVerbose(this.config.name, chatId);
|
|
1463
1510
|
const nextVerbose = !isOn;
|
|
1464
1511
|
this.store.setBotVerbose(this.config.name, chatId, nextVerbose);
|
|
1465
|
-
this.openclawClient.setVerboseTranscriptDelivery(this.
|
|
1512
|
+
this.openclawClient.setVerboseTranscriptDelivery(this.physicalSessionKey(chatId), nextVerbose);
|
|
1466
1513
|
if (isOn) {
|
|
1467
1514
|
await this.replyMessage(messageId, `🔇 ${this.config.name} Verbose 已关闭\n只影响当前 Bot 在当前会话的 Tool call 和中间文本显示`);
|
|
1468
1515
|
}
|
|
@@ -1730,9 +1777,23 @@ export class FeishuBot {
|
|
|
1730
1777
|
if (!activity.followupAdmissionEligible || activity.identityConflict || activity.activeRunId && !noEffectRunId || activity.archived || status.archived ||
|
|
1731
1778
|
!status.sessionId || status.sessionId !== activity.sessionId || !activity.expectedLeafEntryId)
|
|
1732
1779
|
return;
|
|
1733
|
-
return { sessionKey: canonicalPhysicalSessionKey(this.getSessionKey(chatId)), sessionId: status.sessionId, expectedLeafEntryId: activity.expectedLeafEntryId,
|
|
1780
|
+
return { sessionKey: canonicalPhysicalSessionKey(this.getSessionKey(chatId), this.profile), sessionId: status.sessionId, expectedLeafEntryId: activity.expectedLeafEntryId,
|
|
1734
1781
|
...(noEffectRunId ? { nonInjectableRunId: noEffectRunId } : {}) };
|
|
1735
1782
|
}
|
|
1783
|
+
/** How long an unconfirmed input may stay silent before the user is told.
|
|
1784
|
+
*
|
|
1785
|
+
* A fixed 60s window misfired on every slow session: measured reply latency
|
|
1786
|
+
* in a busy group was p50 3.8min / p90 20.2min / max 135min, so 85% of real
|
|
1787
|
+
* replies exceeded the old threshold and every one of them produced a false
|
|
1788
|
+
* "not confirmed" notice. Scale the window with the session's own observed
|
|
1789
|
+
* latency so a healthy slow run is never reported as stuck. */
|
|
1790
|
+
unconfirmedGraceMs(chatId) {
|
|
1791
|
+
const observed = this.store.observedReplyLatencyMs(this.config.name, chatId);
|
|
1792
|
+
const floor = 60_000;
|
|
1793
|
+
if (!observed || !Number.isFinite(observed) || observed <= 0)
|
|
1794
|
+
return floor;
|
|
1795
|
+
return Math.min(Math.max(floor, Math.ceil(observed * 1.5)), 30 * 60_000);
|
|
1796
|
+
}
|
|
1736
1797
|
/** A positive refusal must be durable AND explained to the sender. */
|
|
1737
1798
|
retainChangedFollowup(chatId, item, reason) {
|
|
1738
1799
|
this.store.saveInsertion(this.config.name, chatId, item.rowId, item.insertionId, 'target_changed', { ...item, reason });
|
|
@@ -1749,7 +1810,7 @@ export class FeishuBot {
|
|
|
1749
1810
|
for (const item of this.store.listInsertions(this.config.name, chatId)) {
|
|
1750
1811
|
if (item.state !== 'pending' || item.attempted || item.followupAdmissionGuard || item.ordinaryEligible === false ||
|
|
1751
1812
|
item.ownerId !== saved.id || item.batchId !== saved.batchId || item.stopEpoch !== saved.stopEpoch || item.sessionId !== proof.sessionId ||
|
|
1752
|
-
!item.sessionKey || canonicalPhysicalSessionKey(item.sessionKey) !== proof.sessionKey)
|
|
1813
|
+
!item.sessionKey || canonicalPhysicalSessionKey(item.sessionKey, this.profile) !== proof.sessionKey)
|
|
1753
1814
|
continue;
|
|
1754
1815
|
const source = this.store.getMessageByMessageId(item.messageId);
|
|
1755
1816
|
if (source?.senderType !== 'human' || source.triggerKind !== 'normal')
|
|
@@ -1773,7 +1834,7 @@ export class FeishuBot {
|
|
|
1773
1834
|
!this.queueOwners.get(chatId)?.triggerIds?.has(rowId);
|
|
1774
1835
|
if (!unsubmitted())
|
|
1775
1836
|
return;
|
|
1776
|
-
await this.openclawClient.waitForSessionStop?.(this.
|
|
1837
|
+
await this.openclawClient.waitForSessionStop?.(this.physicalSessionKey(chatId));
|
|
1777
1838
|
if (!unsubmitted())
|
|
1778
1839
|
return;
|
|
1779
1840
|
// Session creation/model setup is not agent admission. Initialize a truly
|
|
@@ -1799,15 +1860,15 @@ export class FeishuBot {
|
|
|
1799
1860
|
}
|
|
1800
1861
|
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1801
1862
|
return;
|
|
1802
|
-
const status = await this.openclawClient.getSessionRuntimeStatus(this.
|
|
1863
|
+
const status = await this.openclawClient.getSessionRuntimeStatus(this.physicalSessionKey(chatId));
|
|
1803
1864
|
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1804
1865
|
return;
|
|
1805
1866
|
const current = this.store.getInsertion(this.config.name, chatId, rowId);
|
|
1806
1867
|
if (current && (!current.activityUnbound || current.attempted))
|
|
1807
1868
|
return;
|
|
1808
|
-
if (current && (current.stopEpoch !== epoch || !current.sessionKey || canonicalPhysicalSessionKey(current.sessionKey) !== canonicalPhysicalSessionKey(this.getSessionKey(chatId))))
|
|
1869
|
+
if (current && (current.stopEpoch !== epoch || !current.sessionKey || canonicalPhysicalSessionKey(current.sessionKey, this.profile) !== canonicalPhysicalSessionKey(this.getSessionKey(chatId), this.profile)))
|
|
1809
1870
|
return;
|
|
1810
|
-
const activity = await this.openclawClient.getSessionDispatchStatus(this.
|
|
1871
|
+
const activity = await this.openclawClient.getSessionDispatchStatus(this.physicalSessionKey(chatId));
|
|
1811
1872
|
if (!unsubmitted() || this.queueRuns.has(chatId) || this.store.getDispatchCustody(this.config.name, chatId))
|
|
1812
1873
|
return;
|
|
1813
1874
|
// Another recovery/ingress lookup may have identified the root and begun
|
|
@@ -1823,6 +1884,13 @@ export class FeishuBot {
|
|
|
1823
1884
|
!!current?.sessionId && !!status.sessionId && current.sessionId !== status.sessionId;
|
|
1824
1885
|
const source = this.store.getMessageByMessageId(messageId);
|
|
1825
1886
|
const ordinaryHuman = source?.id === rowId && source.chatId === chatId && source.senderType === 'human' && source.triggerKind === 'normal';
|
|
1887
|
+
if (current?.followupBlocked && !archived && !generationConflict) {
|
|
1888
|
+
this.store.saveInsertion(this.config.name, chatId, rowId, current.insertionId || JSON.stringify([this.config.appId, chatId, messageId]), 'pending', {
|
|
1889
|
+
...current, followupBlocked: false, followupAdmissionGuard: undefined, followupObservedAt: undefined,
|
|
1890
|
+
reason: 'followup block cleared; awaiting admission re-observation',
|
|
1891
|
+
}, Date.now() + 5000);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1826
1894
|
const proof = ordinaryHuman && compatible && !current?.followupBlocked && !archived ? this.observedFollowupGuard(chatId, status, activity) : undefined;
|
|
1827
1895
|
const previous = current?.followupAdmissionGuard;
|
|
1828
1896
|
if (previous && (archived || generationConflict)) {
|
|
@@ -1871,7 +1939,7 @@ export class FeishuBot {
|
|
|
1871
1939
|
...((archived || generationConflict) ? { followupBlocked: true } : {}),
|
|
1872
1940
|
reason: identified ? 'current Gateway run observed; insertion custody only' : blockerReason,
|
|
1873
1941
|
}, Date.now() + (identified ? 0 : 5000));
|
|
1874
|
-
if (!identified && !proof && current?.unboundSince && Date.now() - current.unboundSince >=
|
|
1942
|
+
if (!identified && !proof && current?.unboundSince && Date.now() - current.unboundSince >= this.unconfirmedGraceMs(chatId) &&
|
|
1875
1943
|
(archived || generationConflict || activity.busyConfirmed || current.restrictedObservation))
|
|
1876
1944
|
this.explainUnconfirmedFollowup(chatId, { ...current, rowId, messageId });
|
|
1877
1945
|
}
|
|
@@ -1886,7 +1954,7 @@ export class FeishuBot {
|
|
|
1886
1954
|
const source = this.store.getMessageByMessageId(item.messageId);
|
|
1887
1955
|
if (item.state !== 'pending' || item.attempted || item.ordinaryEligible === false || !item.sessionId ||
|
|
1888
1956
|
!source || source.id !== item.rowId || source.chatId !== chatId || source.senderType !== 'human' || source.triggerKind !== 'normal' ||
|
|
1889
|
-
!item.sessionKey || canonicalPhysicalSessionKey(item.sessionKey) !== canonicalPhysicalSessionKey(this.getSessionKey(chatId)))
|
|
1957
|
+
!item.sessionKey || canonicalPhysicalSessionKey(item.sessionKey, this.profile) !== canonicalPhysicalSessionKey(this.getSessionKey(chatId), this.profile))
|
|
1890
1958
|
return false;
|
|
1891
1959
|
const admissionEpoch = this.queueAdmissionEpoch.get(chatId) || 0;
|
|
1892
1960
|
const snapshot = this.store.getInsertion(this.config.name, chatId, item.rowId);
|
|
@@ -1936,14 +2004,14 @@ export class FeishuBot {
|
|
|
1936
2004
|
return (this.queueAdmissionEpoch.get(chatId) || 0) === admissionEpoch && this.stopGeneration(chatId) === epoch && saved.stopEpoch === epoch && JSON.stringify(now) === JSON.stringify(saved) &&
|
|
1937
2005
|
this.queueOwners.get(chatId) === local && (!local || local.batch === batch && local.runId === runId && (local.admitted === true || local.yielded === true) && local.collecting === true);
|
|
1938
2006
|
};
|
|
1939
|
-
const activity = await this.openclawClient.getSessionDispatchStatus?.(this.
|
|
2007
|
+
const activity = await this.openclawClient.getSessionDispatchStatus?.(this.physicalSessionKey(chatId)).catch(() => null);
|
|
1940
2008
|
if (!unchanged() || !activity || activity.archived || activity.sessionId !== saved.sessionId)
|
|
1941
2009
|
return false;
|
|
1942
2010
|
if (activity.activeRunId === saved.runId)
|
|
1943
2011
|
return false; // real foreground keeps SDK priority
|
|
1944
2012
|
if (!activity.idleConfirmed && !activity.followupAdmissionEligible)
|
|
1945
2013
|
return false;
|
|
1946
|
-
const status = await this.openclawClient.getSessionRuntimeStatus(this.
|
|
2014
|
+
const status = await this.openclawClient.getSessionRuntimeStatus(this.physicalSessionKey(chatId)).catch(() => null);
|
|
1947
2015
|
if (!unchanged() || !status || status.archived || status.sessionId !== saved.sessionId)
|
|
1948
2016
|
return false;
|
|
1949
2017
|
if (activity.activeRunId && activity.activeRunId !== saved.runId) {
|
|
@@ -1954,7 +2022,7 @@ export class FeishuBot {
|
|
|
1954
2022
|
for (const item of this.store.listInsertions(this.config.name, chatId)) {
|
|
1955
2023
|
if (item.state !== 'pending' || item.attempted || item.noEffectRunId || item.ordinaryEligible === false || item.ownerId !== saved.id || item.batchId !== saved.batchId ||
|
|
1956
2024
|
item.stopEpoch !== epoch || item.sessionId !== saved.sessionId || !item.sessionKey ||
|
|
1957
|
-
canonicalPhysicalSessionKey(item.sessionKey) !== canonicalPhysicalSessionKey(this.getSessionKey(chatId)))
|
|
2025
|
+
canonicalPhysicalSessionKey(item.sessionKey, this.profile) !== canonicalPhysicalSessionKey(this.getSessionKey(chatId), this.profile))
|
|
1958
2026
|
continue;
|
|
1959
2027
|
const source = this.store.getMessageByMessageId(item.messageId);
|
|
1960
2028
|
if (source?.senderType !== 'human' || source.triggerKind !== 'normal')
|
|
@@ -1981,7 +2049,7 @@ export class FeishuBot {
|
|
|
1981
2049
|
let followup;
|
|
1982
2050
|
if (!idle || saved.followupAdmissionGuard) {
|
|
1983
2051
|
const proof = this.observedFollowupGuard(chatId, status, activity);
|
|
1984
|
-
if (!proof || !saved.sessionKey || canonicalPhysicalSessionKey(saved.sessionKey) !== proof.sessionKey)
|
|
2052
|
+
if (!proof || !saved.sessionKey || canonicalPhysicalSessionKey(saved.sessionKey, this.profile) !== proof.sessionKey)
|
|
1985
2053
|
return false;
|
|
1986
2054
|
const previous = saved.followupAdmissionGuard;
|
|
1987
2055
|
if (previous && (previous.sessionKey !== proof.sessionKey || previous.sessionId !== proof.sessionId))
|
|
@@ -2014,7 +2082,12 @@ export class FeishuBot {
|
|
|
2014
2082
|
}
|
|
2015
2083
|
}
|
|
2016
2084
|
stopGeneration(chatId) {
|
|
2017
|
-
|
|
2085
|
+
// A peer bot's group-scoped /stop bumps this bot's durable epoch. The
|
|
2086
|
+
// in-memory value can then be stale and would mask the newer durable one,
|
|
2087
|
+
// so take the greater of the two instead of trusting the cache blindly.
|
|
2088
|
+
const cached = this.stopEpoch.get(chatId);
|
|
2089
|
+
const stored = this.store.getStopGeneration(this.config.name, chatId);
|
|
2090
|
+
return cached === undefined ? stored : Math.max(cached, stored);
|
|
2018
2091
|
}
|
|
2019
2092
|
reactionChecks = new Set();
|
|
2020
2093
|
async receiptReaction(chatId, rowId, messageId, desired, runId = '') {
|
|
@@ -2249,7 +2322,7 @@ export class FeishuBot {
|
|
|
2249
2322
|
}
|
|
2250
2323
|
else {
|
|
2251
2324
|
state = 'pending';
|
|
2252
|
-
if (Date.now() - item.noEffectSince >=
|
|
2325
|
+
if (Date.now() - item.noEffectSince >= this.unconfirmedGraceMs(chatId))
|
|
2253
2326
|
this.explainUnconfirmedFollowup(chatId, item);
|
|
2254
2327
|
}
|
|
2255
2328
|
}
|
|
@@ -2267,7 +2340,7 @@ export class FeishuBot {
|
|
|
2267
2340
|
ordinaryEligible: receipt.ordinaryEligible, attempted: valid ? (receipt.attempted ?? true) : true, sessionId: item.sessionId, reason: receipt.reason, unknownChecks,
|
|
2268
2341
|
unknownSince: state === 'unknown' ? (item.state === 'unknown' && Number.isFinite(item.unknownSince) ? item.unknownSince : Date.now()) : undefined };
|
|
2269
2342
|
this.store.saveInsertion(this.config.name, chatId, item.rowId, item.insertionId, state, record, Date.now() + (state === 'unknown' ? 15_000 : 5000));
|
|
2270
|
-
if (state === 'pending' && !record.attempted && Date.now() - (record.pendingSince || Date.now()) >=
|
|
2343
|
+
if (state === 'pending' && !record.attempted && Date.now() - (record.pendingSince || Date.now()) >= this.unconfirmedGraceMs(chatId))
|
|
2271
2344
|
this.explainUnconfirmedFollowup(chatId, record);
|
|
2272
2345
|
if (state === 'ordinary') {
|
|
2273
2346
|
this.store.deletePendingInsertionNotices(this.config.name, chatId, item.insertionId);
|
|
@@ -2323,7 +2396,7 @@ export class FeishuBot {
|
|
|
2323
2396
|
return process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS !== "0";
|
|
2324
2397
|
}
|
|
2325
2398
|
async getUnhealthySessionStatus(chatId) {
|
|
2326
|
-
const sessionKey = this.
|
|
2399
|
+
const sessionKey = this.physicalSessionKey(chatId);
|
|
2327
2400
|
try {
|
|
2328
2401
|
const resp = await this.openclawClient.getSessionInfo(sessionKey);
|
|
2329
2402
|
const status = String(resp?.session?.status || "").toLowerCase();
|
|
@@ -2505,11 +2578,20 @@ export class FeishuBot {
|
|
|
2505
2578
|
*/
|
|
2506
2579
|
forceExpireQueueOwner(chatId, reason, preserveDispatch = false) {
|
|
2507
2580
|
const owner = this.queueRuns.get(chatId);
|
|
2508
|
-
if (!owner)
|
|
2581
|
+
if (!owner) {
|
|
2582
|
+
// No local owner left, but a persisted dispatch_custody row may still
|
|
2583
|
+
// exist for this chat (it is written before the owner is retired). The
|
|
2584
|
+
// old early return stranded that row forever: it never became visible
|
|
2585
|
+
// again, yet it also was never deleted. Clear it explicitly.
|
|
2586
|
+
if (!preserveDispatch && this.store.clearDispatchCustodyForChat(this.config.name, chatId))
|
|
2587
|
+
console.warn(`[${this.config.name}] cleared orphaned dispatch custody for ${chatId.slice(-8)}: ${reason}`);
|
|
2509
2588
|
return false;
|
|
2589
|
+
}
|
|
2510
2590
|
const context = this.queueOwners.get(chatId);
|
|
2511
2591
|
if (context?.batchId && !preserveDispatch)
|
|
2512
2592
|
this.store.clearDispatchCustody(this.config.name, chatId, context.id, context.batchId);
|
|
2593
|
+
else if (!preserveDispatch && !context)
|
|
2594
|
+
this.store.clearDispatchCustodyForChat(this.config.name, chatId);
|
|
2513
2595
|
if (context)
|
|
2514
2596
|
context.revoked = true;
|
|
2515
2597
|
this.busyChats.set(chatId, 0);
|
|
@@ -2731,7 +2813,11 @@ export class FeishuBot {
|
|
|
2731
2813
|
const expectedIdentitySessionId = identityTargets[0]?.target.sessionId;
|
|
2732
2814
|
try {
|
|
2733
2815
|
if (identityTargets.some(({ target }) => target.state !== 'bound' || !target.sessionId || target.sessionId !== expectedIdentitySessionId ||
|
|
2734
|
-
target
|
|
2816
|
+
// An identity target may hold the physical key (current records) or the
|
|
2817
|
+
// logical key (records written before profiles existed). Both denote
|
|
2818
|
+
// the SAME session, so both are accepted; comparing against only one
|
|
2819
|
+
// form would silently drop the message.
|
|
2820
|
+
(target.sessionKey !== this.physicalSessionKey(chatId) && target.sessionKey !== this.getSessionKey(chatId)) || (target.expiresAt ?? 0) <= Date.now()))
|
|
2735
2821
|
throw new SupersededObservation('retained original target unavailable');
|
|
2736
2822
|
sessionKey = await (expectedIdentitySessionId ? this.ensureSession(chatId, { sessionId: expectedIdentitySessionId, guard }) : this.ensureSession(chatId));
|
|
2737
2823
|
guard();
|
|
@@ -2893,7 +2979,10 @@ export class FeishuBot {
|
|
|
2893
2979
|
if (target?.triggerId === triggerId) {
|
|
2894
2980
|
target.runId = runId;
|
|
2895
2981
|
this.bindRunDeliveryTarget(chatId, runId, target);
|
|
2896
|
-
|
|
2982
|
+
// Result custody must outlive the real run. Observed GPT latency in a heavy
|
|
2983
|
+
// group reached ~59m40s, so a 30-minute release dropped a computed answer
|
|
2984
|
+
// that had no receiver left. Two hours covers the observed tail.
|
|
2985
|
+
this.releaseRunDeliveryTargetAfter(chatId, runId, 2 * 60 * 60_000);
|
|
2897
2986
|
}
|
|
2898
2987
|
// Refine state for logs/idempotent sync rows when OpenClaw returns
|
|
2899
2988
|
// a runId. markSubmittedBatch is idempotent due to local flags and
|
|
@@ -2961,7 +3050,7 @@ export class FeishuBot {
|
|
|
2961
3050
|
guard,
|
|
2962
3051
|
onSendAttempt: submissionId => {
|
|
2963
3052
|
guard();
|
|
2964
|
-
this.store.bindDeliveryRun(this.config.name, chatId, submissionId, { triggerId, messageId: lastHuman.messageId || '', runId: submissionId, stopGeneration: this.stopGeneration(chatId), sessionKey:
|
|
3053
|
+
this.store.bindDeliveryRun(this.config.name, chatId, submissionId, { triggerId, messageId: lastHuman.messageId || '', runId: submissionId, stopGeneration: this.stopGeneration(chatId), sessionKey: canonicalPhysicalSessionKey(sessionKey.replace(/^agent:[^:]+:/, ''), this.profile) });
|
|
2965
3054
|
},
|
|
2966
3055
|
onSubmitted: async (runId) => { guard(); const previousRoot = owner.runId; owner.runId = runId; foregroundRunId = runId; void this.queuedPresentations.advance(liveStatus, previousRoot, runId, owner.id, owner.batchId).catch(() => { }); this.bindRunDeliveryTarget(chatId, runId, { triggerId, messageId: lastHuman.messageId || "", liveStatus }); },
|
|
2967
3056
|
});
|
|
@@ -3050,7 +3139,6 @@ export class FeishuBot {
|
|
|
3050
3139
|
// Mark every merged trigger only after receipt confirmation.
|
|
3051
3140
|
for (const id of mergedTriggerIds)
|
|
3052
3141
|
this.store.markDeliveredReply(this.config.name, chatId, id, lastHuman.messageId);
|
|
3053
|
-
void this.maybeAlertHighContext(chatId);
|
|
3054
3142
|
}
|
|
3055
3143
|
// Go supervision: record the delivered final reply as evidence and
|
|
3056
3144
|
// arm a debounced checker round (busy gates apply inside).
|
|
@@ -3115,6 +3203,10 @@ export class FeishuBot {
|
|
|
3115
3203
|
this.goController?.notifyRunOutcome(chatId, "no_reply", `trigger:${triggerId}`, "(本轮无用户可见回复 NO_REPLY)");
|
|
3116
3204
|
}
|
|
3117
3205
|
console.log(`[${this.config.name}] [${new Date().toISOString()}] ${shouldReply || hasAttachments ? 'Replied' : 'Skipped (empty/NO_REPLY)'} (${reply.length} chars, attachments=${parsedReply.attachments.length})`);
|
|
3206
|
+
// Context usage is re-checked after EVERY completed message (delivered
|
|
3207
|
+
// reply, NO_REPLY turn or runtime failure) and re-notifies each time while
|
|
3208
|
+
// usage stays above the threshold.
|
|
3209
|
+
await this.maybeAlertHighContext(chatId);
|
|
3118
3210
|
guard();
|
|
3119
3211
|
// Replace ack reactions with DONE only for trigger messages actually
|
|
3120
3212
|
// processed in this run. Queued messages that arrived mid-run keep their
|
|
@@ -3256,9 +3348,16 @@ export class FeishuBot {
|
|
|
3256
3348
|
await this.reconcileErrorNotices(chatId);
|
|
3257
3349
|
errorOutcomeConfirmed = this.store.findRunErrorOwner(this.config.name, chatId, p)?.status === 'delivered';
|
|
3258
3350
|
}
|
|
3351
|
+
else if (this.store.hasDeliveredReply(this.config.name, chatId, triggerId)) {
|
|
3352
|
+
// This trigger already received its visible reply. A later terminal
|
|
3353
|
+
// error for the same run must NOT contradict a delivered answer:
|
|
3354
|
+
// the user would see "did not complete" under their own answer.
|
|
3355
|
+
errorOutcomeConfirmed = true;
|
|
3356
|
+
console.warn(`[${this.config.name}] provider_error suppressed for ${chatId.slice(-8)} trigger=${triggerId}: reply already delivered`);
|
|
3357
|
+
}
|
|
3259
3358
|
else
|
|
3260
3359
|
await this.enqueueAndDispatchDelivery(chatId, "provider_error", `trigger:${triggerId}:provider-error`, errorText, [], lastHuman.messageId, `trigger:${triggerId}:provider-error`, undefined, foregroundRunId ? (() => {
|
|
3261
|
-
const p = this.scopeRunError(chatId, { sessionKey:
|
|
3360
|
+
const p = this.scopeRunError(chatId, { sessionKey: canonicalPhysicalSessionKey(this.getSessionKey(chatId), this.profile), sessionId: owner.sessionId, runId: foregroundRunId, source: 'chat', state: 'error', detail: err instanceof Error ? err.message : undefined });
|
|
3262
3361
|
return { sessionKey: p.sessionKey, sessionId: p.sessionId, runId: p.runId, error: p };
|
|
3263
3362
|
})() : undefined)
|
|
3264
3363
|
.then(outcome => {
|
|
@@ -3516,7 +3615,7 @@ export class FeishuBot {
|
|
|
3516
3615
|
}
|
|
3517
3616
|
async readSessionStatus(chatId) {
|
|
3518
3617
|
try {
|
|
3519
|
-
return await this.openclawClient.getSessionRuntimeStatus(this.
|
|
3618
|
+
return await this.openclawClient.getSessionRuntimeStatus(this.physicalSessionKey(chatId));
|
|
3520
3619
|
}
|
|
3521
3620
|
catch {
|
|
3522
3621
|
return normalizeSessionRuntimeStatus(undefined);
|
|
@@ -3529,7 +3628,7 @@ export class FeishuBot {
|
|
|
3529
3628
|
formatUserVisibleError(err, chatId) {
|
|
3530
3629
|
if (err instanceof SessionWaitPaused)
|
|
3531
3630
|
return err.message;
|
|
3532
|
-
return formatProviderError(err, this.config.name, this.config.model, this.chatLocale(chatId));
|
|
3631
|
+
return formatProviderError(err, this.config.name, this.config.model || "", this.chatLocale(chatId));
|
|
3533
3632
|
}
|
|
3534
3633
|
isBridgeControlReply(text) {
|
|
3535
3634
|
const t = text.trim();
|
|
@@ -3614,36 +3713,34 @@ export class FeishuBot {
|
|
|
3614
3713
|
}
|
|
3615
3714
|
}
|
|
3616
3715
|
/**
|
|
3617
|
-
* After a
|
|
3618
|
-
* threshold (default 80%), so they can compact before
|
|
3619
|
-
* timeout/lock/overflow failure modes.
|
|
3620
|
-
*
|
|
3621
|
-
*
|
|
3716
|
+
* After a message completes, alert the user when context usage is at or above
|
|
3717
|
+
* the threshold (default 80%), so they can compact before the session bloats
|
|
3718
|
+
* into the timeout/lock/overflow failure modes.
|
|
3719
|
+
*
|
|
3720
|
+
* The check runs after EVERY completed message and re-notifies on every message
|
|
3721
|
+
* while usage stays above the threshold. There is deliberately no per-chat
|
|
3722
|
+
* "already warned" latch: with one, a single failed send (or a stale latch) would
|
|
3723
|
+
* silently suppress every later warning, and the user would stop being told to
|
|
3724
|
+
* compact while their context keeps growing.
|
|
3622
3725
|
*/
|
|
3623
3726
|
async maybeAlertHighContext(chatId) {
|
|
3624
3727
|
const threshold = contextAlertPct();
|
|
3625
3728
|
if (threshold <= 0)
|
|
3626
3729
|
return;
|
|
3627
3730
|
try {
|
|
3628
|
-
const sessionKey = this.
|
|
3731
|
+
const sessionKey = this.physicalSessionKey(chatId);
|
|
3629
3732
|
const pct = await this.getContextUsagePercent(sessionKey);
|
|
3630
|
-
if (pct <= 0)
|
|
3733
|
+
if (pct <= 0 || pct < threshold)
|
|
3631
3734
|
return;
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
await this.sendMessage(chatId, text).catch(() => { });
|
|
3642
|
-
}
|
|
3643
|
-
else if (pct < threshold - 5) {
|
|
3644
|
-
// Re-arm once usage clearly dropped (hysteresis to avoid flapping).
|
|
3645
|
-
this.contextAlerted.set(chatId, false);
|
|
3646
|
-
}
|
|
3735
|
+
const en = this.isEn(chatId);
|
|
3736
|
+
const text = en
|
|
3737
|
+
? `\u26a0\ufe0f Context is at ${pct}% (\u2265${threshold}%). Consider /compact soon to avoid timeouts/overflow.`
|
|
3738
|
+
: `\u26a0\ufe0f \u4e0a\u4e0b\u6587\u5df2\u8fbe ${pct}%\uff08\u2265${threshold}%\uff09\uff0c\u5efa\u8bae\u5c3d\u5feb /compact \u538b\u7f29\uff0c\u907f\u514d\u8d85\u65f6/\u7206\u4e0a\u4e0b\u6587\u3002`;
|
|
3739
|
+
// Best-effort: a failed alert must not block delivery, but it is logged so
|
|
3740
|
+
// a persistently undelivered warning stays visible instead of silent.
|
|
3741
|
+
await this.sendMessage(chatId, text).catch((err) => {
|
|
3742
|
+
console.warn(`[${this.config.name}] high-context alert send failed for ${chatId.slice(-8)}: ${this.errorSummary(err)}`);
|
|
3743
|
+
});
|
|
3647
3744
|
}
|
|
3648
3745
|
catch {
|
|
3649
3746
|
// best-effort; never block delivery
|
|
@@ -3809,7 +3906,7 @@ export class FeishuBot {
|
|
|
3809
3906
|
this.store.bindDeliveryRun(this.config.name, chatId, runId, { ...persisted, triggerId: target.triggerId, messageId: target.messageId, runId,
|
|
3810
3907
|
sessionId: persisted?.sessionId || (owner?.runId === runId ? owner.sessionId : undefined),
|
|
3811
3908
|
botIdentity: persisted?.botIdentity || this.config.appId, stopGeneration: persisted?.stopGeneration ?? this.stopGeneration(chatId),
|
|
3812
|
-
sessionKey: persisted?.sessionKey ||
|
|
3909
|
+
sessionKey: persisted?.sessionKey || canonicalPhysicalSessionKey(this.getSessionKey(chatId), this.profile) });
|
|
3813
3910
|
this.deliveryTargetsByRun.set(key, {
|
|
3814
3911
|
triggerId: target.triggerId,
|
|
3815
3912
|
messageId: target.messageId,
|
|
@@ -3877,7 +3974,7 @@ export class FeishuBot {
|
|
|
3877
3974
|
this.delayedFailureTimers.set(chatId, timer);
|
|
3878
3975
|
}
|
|
3879
3976
|
deliverySessionKey(chatId) {
|
|
3880
|
-
return this.
|
|
3977
|
+
return this.physicalSessionKey(chatId);
|
|
3881
3978
|
}
|
|
3882
3979
|
stableHash(text) {
|
|
3883
3980
|
let hash = 2166136261;
|
|
@@ -3896,7 +3993,7 @@ export class FeishuBot {
|
|
|
3896
3993
|
}
|
|
3897
3994
|
async enqueueAndDispatchDelivery(chatId, sourceType, sourceId, text, attachments = [], replyToMessageId, deliveryKey, finalReplyModel, provenance, errorNoticeId, completionTriggerIds) {
|
|
3898
3995
|
if (provenance?.sessionKey && !/^agent:/.test(provenance.sessionKey))
|
|
3899
|
-
provenance = { ...provenance, sessionKey:
|
|
3996
|
+
provenance = { ...provenance, sessionKey: canonicalPhysicalSessionKey(provenance.sessionKey, this.profile) };
|
|
3900
3997
|
if (provenance?.sessionKey && provenance.runId && this.store.isStoppedObservation(provenance.sessionKey, provenance.runId))
|
|
3901
3998
|
return { state: "suppressed", outboxIds: [] };
|
|
3902
3999
|
if (text.trim().toUpperCase() === 'NO_REPLY')
|
|
@@ -4066,7 +4163,7 @@ export class FeishuBot {
|
|
|
4066
4163
|
return this.store.getDeliveryDispatchOutcome(this.config.name, chatId, finalDeliveryKey);
|
|
4067
4164
|
}
|
|
4068
4165
|
scopeRunError(chatId, p) {
|
|
4069
|
-
const key =
|
|
4166
|
+
const key = canonicalPhysicalSessionKey(p.sessionKey, this.profile);
|
|
4070
4167
|
const target = this.store.getDeliveryRunTarget(this.config.name, chatId, p.runId);
|
|
4071
4168
|
const observation = this.store.getRunObservation(key, p.runId);
|
|
4072
4169
|
// Pre-upgrade targets may lack app/physical fields. A known exact legacy
|
|
@@ -4091,7 +4188,7 @@ export class FeishuBot {
|
|
|
4091
4188
|
const original = this.store.getDeliveryRunTarget(this.config.name, chatId, p.runId);
|
|
4092
4189
|
if (original?.messageId && this.store.isMessageRecalled(original.messageId) ||
|
|
4093
4190
|
original?.sessionId && p.sessionId && original.sessionId !== p.sessionId ||
|
|
4094
|
-
original?.sessionKey && canonicalPhysicalSessionKey(original.sessionKey) !== canonicalPhysicalSessionKey(p.sessionKey))
|
|
4191
|
+
original?.sessionKey && canonicalPhysicalSessionKey(original.sessionKey, this.profile) !== canonicalPhysicalSessionKey(p.sessionKey, this.profile))
|
|
4095
4192
|
return 'original_error_target_revoked';
|
|
4096
4193
|
const epoch = this.stopGeneration(chatId);
|
|
4097
4194
|
if (p.botIdentity && p.botIdentity !== this.config.appId || original?.botIdentity && original.botIdentity !== this.config.appId ||
|
|
@@ -4377,7 +4474,7 @@ export class FeishuBot {
|
|
|
4377
4474
|
}
|
|
4378
4475
|
const notice = JSON.parse(item.deliveryMetaJson || '{}').insertionNotice;
|
|
4379
4476
|
if (notice) {
|
|
4380
|
-
const info = await this.openclawClient.getSessionInfo(this.
|
|
4477
|
+
const info = await this.openclawClient.getSessionInfo(this.physicalSessionKey(chatId), 1500).catch(() => null);
|
|
4381
4478
|
if (!this.insertionNoticeCurrent(chatId, notice) || info?.session?.archived === true ||
|
|
4382
4479
|
info?.session?.sessionId && info.session.sessionId !== notice.sessionId) {
|
|
4383
4480
|
this.store.markDeliveryFailed(item.id);
|
|
@@ -5901,7 +5998,7 @@ export class FeishuBot {
|
|
|
5901
5998
|
return;
|
|
5902
5999
|
}
|
|
5903
6000
|
if (nextModel === this.config.model) {
|
|
5904
|
-
await this.replyMessage(messageId, `🤖 ${this.config.name} 已经在使用:${this.config.model}`);
|
|
6001
|
+
await this.replyMessage(messageId, `🤖 ${this.config.name} 已经在使用:${this.config.model || `profile ${this.profile}`}`);
|
|
5905
6002
|
return;
|
|
5906
6003
|
}
|
|
5907
6004
|
if (!this.configPath) {
|
|
@@ -5932,7 +6029,7 @@ export class FeishuBot {
|
|
|
5932
6029
|
* Handle /status command: show current session info.
|
|
5933
6030
|
*/
|
|
5934
6031
|
async handleStatusCommand(chatId, chatType, messageId) {
|
|
5935
|
-
const sessionKey = this.
|
|
6032
|
+
const sessionKey = this.physicalSessionKey(chatId);
|
|
5936
6033
|
const chatInfo = this.store.getChatInfo(chatId);
|
|
5937
6034
|
const msgCount = this.store.getMessageCount(chatId);
|
|
5938
6035
|
let session = null;
|
|
@@ -6079,7 +6176,7 @@ export class FeishuBot {
|
|
|
6079
6176
|
}
|
|
6080
6177
|
}
|
|
6081
6178
|
async handleCompactCommand(chatId, messageId, args = "") {
|
|
6082
|
-
const sessionKey = this.
|
|
6179
|
+
const sessionKey = this.physicalSessionKey(chatId);
|
|
6083
6180
|
const en = this.isEn(chatId);
|
|
6084
6181
|
// Argument parsing — modes are strictly separated:
|
|
6085
6182
|
// /compact → transcript tail trim (default, model-free)
|
|
@@ -6180,7 +6277,7 @@ export class FeishuBot {
|
|
|
6180
6277
|
* then verify via describe.
|
|
6181
6278
|
*/
|
|
6182
6279
|
async handleResetCommand(chatId, messageId) {
|
|
6183
|
-
const sessionKey = this.
|
|
6280
|
+
const sessionKey = this.physicalSessionKey(chatId);
|
|
6184
6281
|
try {
|
|
6185
6282
|
await this.assertIdentityEffect();
|
|
6186
6283
|
// Go supervision: a reset replaces the executor session generation; pause
|
|
@@ -6203,7 +6300,7 @@ export class FeishuBot {
|
|
|
6203
6300
|
await new Promise((r) => setTimeout(r, 2000));
|
|
6204
6301
|
this.assertIdentityCurrent();
|
|
6205
6302
|
// A held reset must not initialize an unobserved successor generation.
|
|
6206
|
-
this.initializedSessions.delete(
|
|
6303
|
+
this.initializedSessions.delete(this.getSessionKey(chatId));
|
|
6207
6304
|
if (!held)
|
|
6208
6305
|
await this.ensureSession(chatId);
|
|
6209
6306
|
await this.replyMessage(messageId, `✅ Session 已重置\n模型: ${this.config.model}`);
|
|
@@ -6217,8 +6314,42 @@ export class FeishuBot {
|
|
|
6217
6314
|
* runs for the session, clears the busy lock and every pending trigger, and
|
|
6218
6315
|
* resets stuck reactions so new messages are processed normally again.
|
|
6219
6316
|
*/
|
|
6317
|
+
/** Release every peer bot's in-memory scheduling state for one chat.
|
|
6318
|
+
* Each bot instance keeps its own queueRuns / queueOwners / busyChats /
|
|
6319
|
+
* pendingAckMessages maps; a durable row cleanup cannot reach those. A
|
|
6320
|
+
* group-scoped /stop must retire them too, otherwise the peer's queue owner
|
|
6321
|
+
* survives and its next message is treated as "busy" forever. */
|
|
6322
|
+
releasePeerBotsForChat(chatId) {
|
|
6323
|
+
let released = 0;
|
|
6324
|
+
for (const peer of Array.from(FeishuBot.allBots.values())) {
|
|
6325
|
+
if (peer === this || peer.store !== this.store)
|
|
6326
|
+
continue;
|
|
6327
|
+
// Durable stop epoch was already bumped for this peer by the store-level
|
|
6328
|
+
// group cleanup; mirror it into the peer's in-memory cache.
|
|
6329
|
+
peer.stopEpoch.set(chatId, peer.store.getStopGeneration(peer.config.name, chatId));
|
|
6330
|
+
peer.forceExpireQueueOwner(chatId, 'group-scoped stop by peer bot');
|
|
6331
|
+
peer.busyChats.set(chatId, 0);
|
|
6332
|
+
peer.admittedSteerAcks.delete(chatId);
|
|
6333
|
+
peer.runFailureObservers.delete(chatId);
|
|
6334
|
+
peer.queueRunTokens.delete(chatId);
|
|
6335
|
+
const acks = peer.pendingAckMessages.get(chatId) || [];
|
|
6336
|
+
peer.pendingAckMessages.set(chatId, []);
|
|
6337
|
+
for (const ack of acks)
|
|
6338
|
+
void peer.receiptReaction(chatId, ack.rowId, ack.messageId, '').catch(() => { });
|
|
6339
|
+
// Fence the peer's stale collector so it cannot keep destructively
|
|
6340
|
+
// consuming the replacement batch's frames from the shared session bucket.
|
|
6341
|
+
try {
|
|
6342
|
+
this.openclawClient.revokeSessionCollectors(peer.getSessionKey(chatId), 'group-scoped stop');
|
|
6343
|
+
}
|
|
6344
|
+
catch { /* isolated */ }
|
|
6345
|
+
void peer.openclawClient.cancelSteerSession?.(peer.getSessionKey(chatId), peer.stopGeneration(chatId)).catch(() => { });
|
|
6346
|
+
void peer.openclawClient.abortChat(peer.getSessionKey(chatId)).catch(() => { });
|
|
6347
|
+
released++;
|
|
6348
|
+
}
|
|
6349
|
+
return released;
|
|
6350
|
+
}
|
|
6220
6351
|
async handleStopCommand(chatId, messageId) {
|
|
6221
|
-
const sessionKey = this.
|
|
6352
|
+
const sessionKey = this.physicalSessionKey(chatId);
|
|
6222
6353
|
// Go supervision: a user stop must also pause related Go, so the checker
|
|
6223
6354
|
// cannot wake the just-stopped task back up with a follow-up.
|
|
6224
6355
|
void this.goController?.pauseSilently(chatId, "用户执行了 /stop");
|
|
@@ -6243,14 +6374,29 @@ export class FeishuBot {
|
|
|
6243
6374
|
this.pendingAckMessages.set(chatId, []);
|
|
6244
6375
|
for (const ack of acks)
|
|
6245
6376
|
void this.receiptReaction(chatId, ack.rowId, ack.messageId, '').catch(() => { });
|
|
6377
|
+
// Group scope: a bare /stop in a multi-bot group is executed by ONE bot,
|
|
6378
|
+
// but every bot keeps its own pending_triggers / insertion_custody /
|
|
6379
|
+
// dispatch_custody / reaction rows AND its own in-memory queue owner for
|
|
6380
|
+
// this same chat. Stopping only the executing bot left the others
|
|
6381
|
+
// believing the chat was busy, so their next message was queued and
|
|
6382
|
+
// never dispatched. Release the whole chat for every peer bot.
|
|
6383
|
+
//
|
|
6384
|
+
// This MUST run BEFORE the abort RPC await: a user message that arrives
|
|
6385
|
+
// while the abort is in flight has to survive the stop cutoff, exactly
|
|
6386
|
+
// like the executing bot's own late input above.
|
|
6387
|
+
const group = this.store.stopChatForAllBots(chatId, this.config.name);
|
|
6388
|
+
const peers = this.releasePeerBotsForChat(chatId);
|
|
6246
6389
|
await this.openclawClient.abortChat(sessionKey).catch(() => { });
|
|
6247
6390
|
if (!this.queueRuns.has(chatId))
|
|
6248
6391
|
this.busyChats.set(chatId, 0);
|
|
6249
6392
|
const locale = this.chatLocale(chatId);
|
|
6393
|
+
const scopeNote = peers > 0
|
|
6394
|
+
? (locale === "en" ? ` Also released ${peers} peer bot(s) in this chat.` : ` 同时释放了本群其余 ${peers} 个 bot 的队列。`)
|
|
6395
|
+
: "";
|
|
6250
6396
|
await this.replyMessage(messageId, locale === "en"
|
|
6251
|
-
? `⏹️ Stopped. Aborted the active run, unlocked the queue, and cleared ${cleared} pending message(s). You can send a new message now
|
|
6252
|
-
: `⏹️ 已停止。已中止当前 run、解锁队列,并清掉 ${cleared}
|
|
6253
|
-
console.log(`[${this.config.name}] /stop force-cleared ${chatId.slice(-8)}: aborted run, cleared ${cleared} pending`);
|
|
6397
|
+
? `⏹️ Stopped. Aborted the active run, unlocked the queue, and cleared ${cleared} pending message(s). You can send a new message now.${scopeNote}`
|
|
6398
|
+
: `⏹️ 已停止。已中止当前 run、解锁队列,并清掉 ${cleared} 条待处理消息。现在可以重新发消息了。${scopeNote}`);
|
|
6399
|
+
console.log(`[${this.config.name}] /stop force-cleared ${chatId.slice(-8)}: aborted run, cleared ${cleared} pending, group: insertions=${group.stoppedInsertions} triggers=${group.clearedTriggers} custody=${group.clearedCustody} peers=${peers}`);
|
|
6254
6400
|
}
|
|
6255
6401
|
catch (err) {
|
|
6256
6402
|
await this.replyMessage(messageId, `❌ stop 失败: ${err.message}`);
|
package/dist/go-controller.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export type GoClient = {
|
|
|
75
75
|
export type GoControllerDeps = {
|
|
76
76
|
botName: string;
|
|
77
77
|
fallbackModel: string;
|
|
78
|
+
/** OpenClaw profile that owns this bot's sessions (default "main"). */
|
|
79
|
+
profile?: string;
|
|
78
80
|
store: import("./message-store.js").MessageStore;
|
|
79
81
|
client: GoClient;
|
|
80
82
|
/** Executor session key for a chat (e.g. lma-<bot>-<chatId>). */
|
package/dist/go-controller.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/** Independent LMA supervision. MessageStore owns objective, criteria and completion. */
|
|
2
2
|
import { goSnapshotBlocked } from "./go-runtime-gate.js";
|
|
3
|
+
/** Logical (profile-free) part of a session key. */
|
|
4
|
+
const withoutAgentPrefix = (key) => key.replace(/^agent:[^:]+:/, '');
|
|
5
|
+
/** Physical session key for a logical executor key under this bot's profile. */
|
|
6
|
+
const physicalKey = (key, profile) => {
|
|
7
|
+
if (/^agent:[^:]+:/.test(key))
|
|
8
|
+
return key;
|
|
9
|
+
const agent = String(profile || "main").trim() || "main";
|
|
10
|
+
return `agent:${agent}:${key}`;
|
|
11
|
+
};
|
|
3
12
|
const CHECK_DELAY_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_GO_CHECK_DELAY_MS || 20000);
|
|
4
13
|
const CHECKER_TIMEOUT_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_GO_CHECKER_TIMEOUT_MS || 10 * 60 * 1000);
|
|
5
14
|
const MAX_CONTINUES = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_GO_MAX_CONTINUES || 10);
|
|
@@ -120,7 +129,7 @@ export class GoController {
|
|
|
120
129
|
const executor = sessionKey === job.executorSessionKey;
|
|
121
130
|
const result = executor ? await client.collectGoExecutorRun({ runId: receipt.runId, sessionKey, sessionId: expectedSessionId, timeoutMs: CHECKER_TIMEOUT_MS }) : null;
|
|
122
131
|
if (result && (result.runId !== receipt.runId || result.sessionId !== expectedSessionId ||
|
|
123
|
-
(result.sessionKey !== sessionKey && result.sessionKey !==
|
|
132
|
+
(result.sessionKey !== sessionKey && result.sessionKey !== physicalKey(sessionKey, this.d.profile))))
|
|
124
133
|
throw new Error("Executor result identity mismatch");
|
|
125
134
|
const text = result ? result.text : await client.collectGoRun({ runId: receipt.runId, sessionKey, sessionId: expectedSessionId, timeoutMs: CHECKER_TIMEOUT_MS });
|
|
126
135
|
this.d.store.recordGoRunResult(jobId, actionKey, { state: "complete", runId: receipt.runId, text, ...(result ? { executorResult: result } : {}) });
|
|
@@ -204,7 +213,10 @@ export class GoController {
|
|
|
204
213
|
await this.d.reply(messageId, `⚠️ 当前已有活动 Go #${existing.id}:\n目标:${existing.goal.slice(0, 200)}\n如需更换目标,请先 /go stop。`);
|
|
205
214
|
return;
|
|
206
215
|
}
|
|
207
|
-
|
|
216
|
+
// The Go supervision path talks to the Gateway directly, so it uses the
|
|
217
|
+
// PHYSICAL key throughout (profile included) exactly like the executor
|
|
218
|
+
// session itself.
|
|
219
|
+
const executorKey = physicalKey(this.d.executorSessionKeyFor(chatId), this.d.profile);
|
|
208
220
|
if (await this.safetyUnavailable(executorKey)) {
|
|
209
221
|
await this.d.reply(messageId, "❌ Go 未启动:当前运行时尚未验证 Checker 工具隔离、精确子孙任务/投递门禁及条件发送。不能仅凭提示词安全开启自动监督。独立模式无需原生 Goal;普通聊天不受影响。");
|
|
210
222
|
return;
|
|
@@ -230,7 +242,10 @@ export class GoController {
|
|
|
230
242
|
|| (typeof session.model === "string" && session.model
|
|
231
243
|
? (typeof session.modelProvider === "string" && session.modelProvider && !session.model.includes("/") ? `${session.modelProvider}/${session.model}` : session.model)
|
|
232
244
|
: this.d.fallbackModel);
|
|
233
|
-
|
|
245
|
+
// The checker session NAME stays profile-free (lma- -> lma-go-), exactly like
|
|
246
|
+
// every other logical key LMA stores. It is addressed under the bot's
|
|
247
|
+
// profile only when it is actually handed to the Gateway.
|
|
248
|
+
const checkerKey = physicalKey(`${withoutAgentPrefix(executorKey).replace(/^lma-/, "lma-go-")}-${crypto.randomUUID()}`, this.d.profile);
|
|
234
249
|
try {
|
|
235
250
|
await this.d.client.createSession({ key: checkerKey, model: resolvedModel, label: `Go Checker: ${this.d.botName} ${chatId.slice(-6)}` });
|
|
236
251
|
}
|
|
@@ -349,7 +364,7 @@ export class GoController {
|
|
|
349
364
|
if (saved?.state !== "complete") {
|
|
350
365
|
const result = await this.d.client.readGoExecutorRunResult?.({ ...send, runId: receipt.runId });
|
|
351
366
|
if (!result || result.runId !== receipt.runId || result.sessionId !== job.executorSessionId ||
|
|
352
|
-
![job.executorSessionKey,
|
|
367
|
+
![job.executorSessionKey, physicalKey(job.executorSessionKey, this.d.profile)].includes(result.sessionKey)) {
|
|
353
368
|
this.d.store.recordGoRunResult(job.id, send.actionKey, { state: "unknown", runId: receipt.runId });
|
|
354
369
|
continue;
|
|
355
370
|
}
|
|
@@ -510,16 +525,17 @@ export class GoController {
|
|
|
510
525
|
// ---------------- check cycle ----------------
|
|
511
526
|
async executorBusy(chatId, job) {
|
|
512
527
|
try {
|
|
513
|
-
|
|
528
|
+
const executorPhysicalKey = job.executorSessionKey;
|
|
529
|
+
if (await this.safetyUnavailable(executorPhysicalKey))
|
|
514
530
|
return "运行时安全能力未验证";
|
|
515
|
-
const info = await (this.d.client.getGoSessionInfo?.bind(this.d.client) ?? this.d.client.getSessionInfo.bind(this.d.client))(
|
|
531
|
+
const info = await (this.d.client.getGoSessionInfo?.bind(this.d.client) ?? this.d.client.getSessionInfo.bind(this.d.client))(executorPhysicalKey);
|
|
516
532
|
if (info?.session?.sessionId !== job.executorSessionId || info?.session?.archived)
|
|
517
533
|
return "执行会话代际已改变或归档";
|
|
518
534
|
const identity = this.d.store.getGoNativeIdentity(job.id);
|
|
519
535
|
if (!identity)
|
|
520
536
|
return "Missing independent run binding";
|
|
521
|
-
const snapshot = await this.d.client.readGoRuntimeSnapshot(
|
|
522
|
-
const blocked = goSnapshotBlocked(snapshot, { sessionKey:
|
|
537
|
+
const snapshot = await this.d.client.readGoRuntimeSnapshot(executorPhysicalKey, job.executorSessionId);
|
|
538
|
+
const blocked = goSnapshotBlocked(snapshot, { sessionKey: executorPhysicalKey, sessionId: job.executorSessionId,
|
|
523
539
|
goalId: identity.nativeGoalId, goalRevision: identity.goalRevision, executorRunIds: identity.executorRunIds });
|
|
524
540
|
if (blocked) {
|
|
525
541
|
if (blocked === "identity/version changed")
|
package/dist/message-store.d.ts
CHANGED
|
@@ -171,7 +171,7 @@ export declare class MessageStore {
|
|
|
171
171
|
recoverUnsentReservations(bot: string): number;
|
|
172
172
|
saveAdmission(sessionKey: string, submissionId: string, state: string, record: object): void;
|
|
173
173
|
listAdmissions(sessionKey: string): any[];
|
|
174
|
-
stopExactRun(sessionKey: string, runId: string): void;
|
|
174
|
+
stopExactRun(sessionKey: string, runId: string, userStopped?: boolean): void;
|
|
175
175
|
isStoppedObservation(sessionKey: string, runId: string): boolean;
|
|
176
176
|
hasUnconfirmedRunDelivery(bot: string, chat: string, runId: string): boolean;
|
|
177
177
|
/** UI-only witness of an exact run's visible reply in the original Bot/chat.
|
|
@@ -190,6 +190,27 @@ export declare class MessageStore {
|
|
|
190
190
|
hasYieldedObservation(sessionKey: string): boolean;
|
|
191
191
|
getRunObservation(sessionKey: string, runId: string): any;
|
|
192
192
|
runObservationState(sessionKey: string, runId: string): string | undefined;
|
|
193
|
+
/** Observed reply latency for this chat, from real delivered replies.
|
|
194
|
+
*
|
|
195
|
+
* Used to size the "input still unconfirmed" window. A fixed short timer
|
|
196
|
+
* misfired on slow sessions where nearly every legitimate reply took longer
|
|
197
|
+
* than the timer, producing a false "not confirmed" notice for a run that
|
|
198
|
+
* was in fact progressing normally. Returns null when there is no history. */
|
|
199
|
+
observedReplyLatencyMs(bot: string, chat: string): number | null;
|
|
200
|
+
/** True only when THIS run was explicitly stopped by the user.
|
|
201
|
+
*
|
|
202
|
+
* Distinct from a retired observation: a group /stop retires every bot's
|
|
203
|
+
* observation on the shared session, which is NOT a user request to kill
|
|
204
|
+
* each individual run. */
|
|
205
|
+
isExplicitlyStoppedObservation(sessionKey: string, runId: string): boolean;
|
|
206
|
+
/** Retire session-scoped observation, but NEVER a run whose committed
|
|
207
|
+
* terminal reply has already been observed.
|
|
208
|
+
*
|
|
209
|
+
* A run with terminalObserved=true holds a COMPUTED answer. Retiring its
|
|
210
|
+
* observation (via /stop, /reset or an abort) removes the only durable
|
|
211
|
+
* custody that answer has, so recoverExactRuns can never deliver it and the
|
|
212
|
+
* computed reply is silently lost. A run without a terminal observation has
|
|
213
|
+
* nothing to deliver yet and may be retired normally. */
|
|
193
214
|
stopSessionObservations(sessionKey: string): void;
|
|
194
215
|
settleExactRunReactions(sessionKey: string, runId: string): void;
|
|
195
216
|
finishRunObservation(sessionKey: string, runId: string, state?: string): void;
|
|
@@ -211,6 +232,23 @@ export declare class MessageStore {
|
|
|
211
232
|
isInsertionReserved(bot: string, chat: string, row: number): boolean;
|
|
212
233
|
stopInsertionAtEpoch(bot: string, chat: string, row: number, epoch: number): void;
|
|
213
234
|
stopInsertions(bot: string, chat: string): void;
|
|
235
|
+
/** Group-scoped stop cleanup: every bot in this chat, not just the caller.
|
|
236
|
+
* A bare /stop in a multi-bot group is executed by ONE bot, but each bot keeps
|
|
237
|
+
* its own pending_triggers / insertion_custody / dispatch_custody / reaction
|
|
238
|
+
* rows for the same chat. Stopping only the executing bot left the others
|
|
239
|
+
* believing the chat was still busy, so their next message was queued and
|
|
240
|
+
* never dispatched. */
|
|
241
|
+
stopChatForAllBots(chat: string, exceptBot?: string): {
|
|
242
|
+
stoppedInsertions: number;
|
|
243
|
+
clearedTriggers: number;
|
|
244
|
+
clearedCustody: number;
|
|
245
|
+
};
|
|
246
|
+
/** Same predicate as stopInsertions, but reports how many rows it retired. */
|
|
247
|
+
stopInsertionsCount(bot: string, chat: string): number;
|
|
248
|
+
/** Drop a bot's dispatch custody for a chat regardless of owner/batch
|
|
249
|
+
* identity. forceExpireQueueOwner can no-op when the local queue owner is
|
|
250
|
+
* already gone, which used to strand the row forever. */
|
|
251
|
+
clearDispatchCustodyForChat(bot: string, chat: string): number;
|
|
214
252
|
hasUnknownDeliveryOperation(outbox: number): boolean;
|
|
215
253
|
/** Includes non-visible uploads/doc creation: absence of a message_id is
|
|
216
254
|
* not proof that no SDK effect has been attempted or accepted. */
|
package/dist/message-store.js
CHANGED
|
@@ -258,9 +258,10 @@ export class MessageStore {
|
|
|
258
258
|
listAdmissions(sessionKey) {
|
|
259
259
|
return this.db.prepare('SELECT * FROM admission_custody WHERE session_key=? ORDER BY rowid DESC LIMIT 100').all(sessionKey).map(r => ({ ...JSON.parse(r.record_json), submissionId: r.submission_id, state: r.state }));
|
|
260
260
|
}
|
|
261
|
-
stopExactRun(sessionKey, runId) {
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
stopExactRun(sessionKey, runId, userStopped = false) {
|
|
262
|
+
const record = userStopped ? { userStopped: true } : {};
|
|
263
|
+
this.db.prepare(`INSERT INTO run_observations VALUES(?,?,?,'stopped',0)
|
|
264
|
+
ON CONFLICT(session_key,run_id) DO UPDATE SET state='stopped', record_json=json_patch(run_observations.record_json,excluded.record_json)`).run(sessionKey, runId, JSON.stringify(record));
|
|
264
265
|
}
|
|
265
266
|
isStoppedObservation(sessionKey, runId) {
|
|
266
267
|
return !!this.db.prepare("SELECT 1 FROM run_observations WHERE session_key=? AND run_id=? AND state='stopped'").get(sessionKey, runId);
|
|
@@ -329,8 +330,45 @@ export class MessageStore {
|
|
|
329
330
|
runObservationState(sessionKey, runId) {
|
|
330
331
|
return this.db.prepare('SELECT state FROM run_observations WHERE session_key=? AND run_id=?').get(sessionKey, runId)?.state;
|
|
331
332
|
}
|
|
333
|
+
/** Observed reply latency for this chat, from real delivered replies.
|
|
334
|
+
*
|
|
335
|
+
* Used to size the "input still unconfirmed" window. A fixed short timer
|
|
336
|
+
* misfired on slow sessions where nearly every legitimate reply took longer
|
|
337
|
+
* than the timer, producing a false "not confirmed" notice for a run that
|
|
338
|
+
* was in fact progressing normally. Returns null when there is no history. */
|
|
339
|
+
observedReplyLatencyMs(bot, chat) {
|
|
340
|
+
// p90 over real delivered replies. MAX would be dominated by a single
|
|
341
|
+
// pathological outlier and inflate the window for every later request.
|
|
342
|
+
const sql = "SELECT (d.delivered_at - m.timestamp) AS lat FROM delivered_replies d" +
|
|
343
|
+
" JOIN messages m ON m.id=d.trigger_message_row_id" +
|
|
344
|
+
" WHERE d.bot_name=? AND d.chat_id=? AND d.delivered_at > m.timestamp AND m.timestamp > 0" +
|
|
345
|
+
" ORDER BY d.delivered_at DESC LIMIT 30";
|
|
346
|
+
const rows = this.db.prepare(sql).all(bot, chat);
|
|
347
|
+
const lats = rows.map(r => Number(r.lat)).filter(x => Number.isFinite(x) && x > 0).sort((a, b) => a - b);
|
|
348
|
+
if (!lats.length)
|
|
349
|
+
return null;
|
|
350
|
+
const p90 = lats[Math.min(lats.length - 1, Math.floor(lats.length * 0.9))];
|
|
351
|
+
return Number.isFinite(p90) && p90 > 0 ? p90 : null;
|
|
352
|
+
}
|
|
353
|
+
/** True only when THIS run was explicitly stopped by the user.
|
|
354
|
+
*
|
|
355
|
+
* Distinct from a retired observation: a group /stop retires every bot's
|
|
356
|
+
* observation on the shared session, which is NOT a user request to kill
|
|
357
|
+
* each individual run. */
|
|
358
|
+
isExplicitlyStoppedObservation(sessionKey, runId) {
|
|
359
|
+
const row = this.db.prepare("SELECT 1 FROM run_observations WHERE session_key=? AND run_id=? AND state='stopped' AND json_extract(record_json,'$.userStopped')=1").get(sessionKey, runId);
|
|
360
|
+
return !!row;
|
|
361
|
+
}
|
|
362
|
+
/** Retire session-scoped observation, but NEVER a run whose committed
|
|
363
|
+
* terminal reply has already been observed.
|
|
364
|
+
*
|
|
365
|
+
* A run with terminalObserved=true holds a COMPUTED answer. Retiring its
|
|
366
|
+
* observation (via /stop, /reset or an abort) removes the only durable
|
|
367
|
+
* custody that answer has, so recoverExactRuns can never deliver it and the
|
|
368
|
+
* computed reply is silently lost. A run without a terminal observation has
|
|
369
|
+
* nothing to deliver yet and may be retired normally. */
|
|
332
370
|
stopSessionObservations(sessionKey) {
|
|
333
|
-
this.db.prepare("UPDATE run_observations SET state='stopped' WHERE session_key=? AND state IN ('observing','outbox_owned')").run(sessionKey);
|
|
371
|
+
this.db.prepare("UPDATE run_observations SET state='stopped' WHERE session_key=? AND state IN ('observing','outbox_owned') AND json_extract(record_json,'$.terminalObserved') IS NOT 1").run(sessionKey);
|
|
334
372
|
}
|
|
335
373
|
settleExactRunReactions(sessionKey, runId) {
|
|
336
374
|
const targets = this.db.prepare('SELECT bot_name,chat_id FROM run_delivery_targets WHERE run_id=?').all(runId);
|
|
@@ -406,6 +444,45 @@ export class MessageStore {
|
|
|
406
444
|
stopInsertions(bot, chat) {
|
|
407
445
|
this.db.prepare("UPDATE insertion_custody SET state='stopped' WHERE bot_name=? AND chat_id=? AND (state NOT IN ('committed','ordinary') OR state='ordinary' AND json_type(record_json,'$.followupAdmissionGuard')='object')").run(bot, chat);
|
|
408
446
|
}
|
|
447
|
+
/** Group-scoped stop cleanup: every bot in this chat, not just the caller.
|
|
448
|
+
* A bare /stop in a multi-bot group is executed by ONE bot, but each bot keeps
|
|
449
|
+
* its own pending_triggers / insertion_custody / dispatch_custody / reaction
|
|
450
|
+
* rows for the same chat. Stopping only the executing bot left the others
|
|
451
|
+
* believing the chat was still busy, so their next message was queued and
|
|
452
|
+
* never dispatched. */
|
|
453
|
+
stopChatForAllBots(chat, exceptBot) {
|
|
454
|
+
const bots = this.db.prepare("SELECT DISTINCT bot_name FROM pending_triggers WHERE chat_id=? UNION SELECT DISTINCT bot_name FROM insertion_custody WHERE chat_id=? UNION SELECT DISTINCT bot_name FROM dispatch_custody WHERE chat_id=? UNION SELECT DISTINCT bot_name FROM reaction_custody WHERE chat_id=?").all(chat, chat, chat, chat).map((r) => r.bot_name);
|
|
455
|
+
let stoppedInsertions = 0, clearedTriggers = 0, clearedCustody = 0;
|
|
456
|
+
for (const bot of bots) {
|
|
457
|
+
if (bot === exceptBot) {
|
|
458
|
+
// The executing bot already bumped its own epoch and cancelled its own
|
|
459
|
+
// reaction intents; touching them again would desynchronize its
|
|
460
|
+
// in-memory generation from the durable one.
|
|
461
|
+
stoppedInsertions += this.stopInsertionsCount(bot, chat);
|
|
462
|
+
clearedTriggers += this.clearAllPendingTriggers(bot, chat);
|
|
463
|
+
clearedCustody += this.clearDispatchCustodyForChat(bot, chat);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
// Invalidate every epoch-scoped record for this bot/chat: a stale
|
|
467
|
+
// dispatch_custody with an old stopEpoch can never be released later.
|
|
468
|
+
this.bumpStopGeneration(bot, chat);
|
|
469
|
+
this.cancelReactionIntents(bot, chat, this.getStopGeneration(bot, chat));
|
|
470
|
+
stoppedInsertions += this.stopInsertionsCount(bot, chat);
|
|
471
|
+
clearedTriggers += this.clearAllPendingTriggers(bot, chat);
|
|
472
|
+
clearedCustody += this.clearDispatchCustodyForChat(bot, chat);
|
|
473
|
+
}
|
|
474
|
+
return { stoppedInsertions, clearedTriggers, clearedCustody };
|
|
475
|
+
}
|
|
476
|
+
/** Same predicate as stopInsertions, but reports how many rows it retired. */
|
|
477
|
+
stopInsertionsCount(bot, chat) {
|
|
478
|
+
return this.db.prepare("UPDATE insertion_custody SET state='stopped' WHERE bot_name=? AND chat_id=? AND (state NOT IN ('committed','ordinary') OR state='ordinary' AND json_type(record_json,'$.followupAdmissionGuard')='object')").run(bot, chat).changes;
|
|
479
|
+
}
|
|
480
|
+
/** Drop a bot's dispatch custody for a chat regardless of owner/batch
|
|
481
|
+
* identity. forceExpireQueueOwner can no-op when the local queue owner is
|
|
482
|
+
* already gone, which used to strand the row forever. */
|
|
483
|
+
clearDispatchCustodyForChat(bot, chat) {
|
|
484
|
+
return this.db.prepare("DELETE FROM dispatch_custody WHERE bot_name=? AND chat_id=?").run(bot, chat).changes;
|
|
485
|
+
}
|
|
409
486
|
hasUnknownDeliveryOperation(outbox) {
|
|
410
487
|
return !!this.db.prepare(`SELECT 1 FROM delivery_operations o WHERE outbox_id=? AND state IN ('attempted','unknown')
|
|
411
488
|
AND NOT EXISTS(SELECT 1 FROM delivery_receipt_custody k WHERE k.outbox_id=o.outbox_id AND k.operation_key=o.operation_key) LIMIT 1`).get(outbox);
|
|
@@ -72,10 +72,6 @@ export type ChatAttachment = {
|
|
|
72
72
|
};
|
|
73
73
|
export declare const GATEWAY_PROTOCOL_MIN = 4;
|
|
74
74
|
export declare const GATEWAY_PROTOCOL_MAX = 4;
|
|
75
|
-
/**
|
|
76
|
-
* OpenClaw Gateway WebSocket client.
|
|
77
|
-
* Full agent pipeline — tools, memory, skills, context management by OpenClaw.
|
|
78
|
-
*/
|
|
79
75
|
export declare class OpenClawClient {
|
|
80
76
|
private goEvidenceObservers;
|
|
81
77
|
/** Raw full event observer. Observation is NOT a Feishu delivery receipt. */
|
package/dist/openclaw-client.js
CHANGED
|
@@ -80,6 +80,15 @@ const terminalReason = (...values) => values.map(closedStopReason).sort((a, b) =
|
|
|
80
80
|
* OpenClaw Gateway WebSocket client.
|
|
81
81
|
* Full agent pipeline — tools, memory, skills, context management by OpenClaw.
|
|
82
82
|
*/
|
|
83
|
+
/** Physical session key for a logical key.
|
|
84
|
+
*
|
|
85
|
+
* A key that already carries an explicit `agent:<id>:` prefix keeps it, so a
|
|
86
|
+
* client shared by several bots can serve sessions owned by different
|
|
87
|
+
* OpenClaw profiles. A key without a prefix stays on the historical `main`
|
|
88
|
+
* profile, so existing configurations behave exactly as before. */
|
|
89
|
+
const withAgentPrefix = (key) => /^agent:[^:]+:/.test(key) ? key : `agent:main:${key}`;
|
|
90
|
+
/** Logical (profile-free) part of a session key. */
|
|
91
|
+
const withoutAgentPrefix = (key) => key.replace(/^agent:[^:]+:/, '');
|
|
83
92
|
export class OpenClawClient {
|
|
84
93
|
goEvidenceObservers = new Set();
|
|
85
94
|
/** Raw full event observer. Observation is NOT a Feishu delivery receipt. */
|
|
@@ -153,7 +162,11 @@ export class OpenClawClient {
|
|
|
153
162
|
isExplicitlyStoppedRun(sessionKey, runId) {
|
|
154
163
|
if (!runId)
|
|
155
164
|
return false;
|
|
156
|
-
|
|
165
|
+
// A retired observation is NOT the same as a user-requested stop. A bare
|
|
166
|
+
// /stop in a multi-bot group retires EVERY bot's observation on the shared
|
|
167
|
+
// session; treating that retirement as "the user stopped this run"
|
|
168
|
+
// discarded answers the Gateway had already computed.
|
|
169
|
+
if (this.recoveryStore?.isExplicitlyStoppedObservation(this.canonicalSessionKey(sessionKey), runId))
|
|
157
170
|
return true;
|
|
158
171
|
const key = JSON.stringify([this.canonicalSessionKey(sessionKey), runId]);
|
|
159
172
|
const at = this.explicitlyStoppedRuns.get(key);
|
|
@@ -1255,7 +1268,7 @@ export class OpenClawClient {
|
|
|
1255
1268
|
// tripping the disconnect path.
|
|
1256
1269
|
const startedConnected = this.connected;
|
|
1257
1270
|
let chatFinalText = "";
|
|
1258
|
-
let sessionKey = targetSessionKey ?
|
|
1271
|
+
let sessionKey = targetSessionKey ? withAgentPrefix(targetSessionKey) : "";
|
|
1259
1272
|
let shortSessionKey = targetSessionKey ? targetSessionKey.replace(/^agent:[^:]+:/, "") : "";
|
|
1260
1273
|
let chatFinalTimer = null;
|
|
1261
1274
|
let lifecycleEndTimer = null;
|
|
@@ -2227,7 +2240,7 @@ export class OpenClawClient {
|
|
|
2227
2240
|
return sessionKey.replace(/^agent:[^:]+:/, "");
|
|
2228
2241
|
}
|
|
2229
2242
|
canonicalSessionKey(sessionKey) {
|
|
2230
|
-
return
|
|
2243
|
+
return withAgentPrefix(sessionKey);
|
|
2231
2244
|
}
|
|
2232
2245
|
registerPendingSteer(sessionKey, text, displayText) {
|
|
2233
2246
|
const key = this.shortKey(sessionKey);
|
|
@@ -2533,7 +2546,7 @@ export class OpenClawClient {
|
|
|
2533
2546
|
const message = firstErr.message || String(firstErr);
|
|
2534
2547
|
if (!/session (?:not found|unknown)|unknown session|invalid session key/i.test(message))
|
|
2535
2548
|
throw firstErr;
|
|
2536
|
-
const fullKey = sessionKey
|
|
2549
|
+
const fullKey = withAgentPrefix(sessionKey);
|
|
2537
2550
|
held?.guard();
|
|
2538
2551
|
await this.patchSession({ key: fullKey, model: expectedModel, ...(held ? { expectedSessionId: held.sessionId } : {}) });
|
|
2539
2552
|
}
|
|
@@ -2743,7 +2756,7 @@ export class OpenClawClient {
|
|
|
2743
2756
|
for (const stopped of result.runIds)
|
|
2744
2757
|
if (typeof stopped === "string" && stopped) {
|
|
2745
2758
|
this.explicitlyStoppedRuns.set(JSON.stringify([key, stopped]), Date.now());
|
|
2746
|
-
this.recoveryStore?.stopExactRun(key, stopped);
|
|
2759
|
+
this.recoveryStore?.stopExactRun(key, stopped, true);
|
|
2747
2760
|
}
|
|
2748
2761
|
}
|
|
2749
2762
|
return result;
|
|
@@ -2978,8 +2991,8 @@ export class OpenClawClient {
|
|
|
2978
2991
|
}
|
|
2979
2992
|
}
|
|
2980
2993
|
muteProactiveDelivery(sessionKey) {
|
|
2981
|
-
const shortKey =
|
|
2982
|
-
const fullKey =
|
|
2994
|
+
const shortKey = withoutAgentPrefix(sessionKey);
|
|
2995
|
+
const fullKey = withAgentPrefix(sessionKey);
|
|
2983
2996
|
const keys = [shortKey, fullKey];
|
|
2984
2997
|
for (const key of keys)
|
|
2985
2998
|
this.addMutedProactiveKey(key);
|
|
@@ -3642,7 +3655,7 @@ export class OpenClawClient {
|
|
|
3642
3655
|
if (onSettled)
|
|
3643
3656
|
this.sessionSettlementCallbacks.set(this.canonicalSessionKey(sessionKey), onSettled);
|
|
3644
3657
|
this.sessionMessageCallbacks.set(sessionKey, onMessage);
|
|
3645
|
-
this.sessionMessageCallbacks.set(
|
|
3658
|
+
this.sessionMessageCallbacks.set(withAgentPrefix(sessionKey), onMessage);
|
|
3646
3659
|
this.subscribedKeys.add(sessionKey);
|
|
3647
3660
|
try {
|
|
3648
3661
|
await this.rpc("sessions.messages.subscribe", { key: this.canonicalSessionKey(sessionKey) });
|
|
@@ -3652,7 +3665,7 @@ export class OpenClawClient {
|
|
|
3652
3665
|
}
|
|
3653
3666
|
}
|
|
3654
3667
|
setVerboseTranscriptDelivery(sessionKey, enabled) {
|
|
3655
|
-
const keys = [sessionKey,
|
|
3668
|
+
const keys = [sessionKey, withAgentPrefix(sessionKey)];
|
|
3656
3669
|
for (const key of keys) {
|
|
3657
3670
|
if (enabled)
|
|
3658
3671
|
this.verboseTranscriptSessions.add(key);
|
|
@@ -3665,9 +3678,9 @@ export class OpenClawClient {
|
|
|
3665
3678
|
async unsubscribeSession(sessionKey) {
|
|
3666
3679
|
this.sessionSettlementCallbacks.delete(this.canonicalSessionKey(sessionKey));
|
|
3667
3680
|
this.sessionMessageCallbacks.delete(sessionKey);
|
|
3668
|
-
this.sessionMessageCallbacks.delete(
|
|
3681
|
+
this.sessionMessageCallbacks.delete(withAgentPrefix(sessionKey));
|
|
3669
3682
|
this.toolEventCallbacks.delete(sessionKey);
|
|
3670
|
-
this.toolEventCallbacks.delete(
|
|
3683
|
+
this.toolEventCallbacks.delete(withAgentPrefix(sessionKey));
|
|
3671
3684
|
this.setVerboseTranscriptDelivery(sessionKey, false);
|
|
3672
3685
|
this.subscribedKeys.delete(sessionKey);
|
|
3673
3686
|
try {
|
|
@@ -3682,6 +3695,6 @@ export class OpenClawClient {
|
|
|
3682
3695
|
*/
|
|
3683
3696
|
onToolEvent(sessionKey, callback) {
|
|
3684
3697
|
this.toolEventCallbacks.set(sessionKey, callback);
|
|
3685
|
-
this.toolEventCallbacks.set(
|
|
3698
|
+
this.toolEventCallbacks.set(withAgentPrefix(sessionKey), callback);
|
|
3686
3699
|
}
|
|
3687
3700
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-lark-multi-agent",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.21",
|
|
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": {
|
|
@@ -89,4 +89,4 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
}
|
|
92
|
-
}
|
|
92
|
+
}
|