openclaw-lark-multi-agent 1.4.3 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -19
- package/README.zh-CN.md +14 -0
- package/dist/cli.js +4 -4
- package/dist/compact-progress.d.ts +4 -4
- package/dist/compact-progress.js +5 -5
- package/dist/feishu-bot.d.ts +28 -14
- package/dist/feishu-bot.js +322 -282
- package/dist/message-store.d.ts +4 -3
- package/dist/openclaw-client.d.ts +32 -25
- package/dist/openclaw-client.js +327 -158
- package/package.json +2 -2
- package/plugins/lma-steer/README.md +23 -30
- package/dist/session-file-compactor.d.ts +0 -39
- package/dist/session-file-compactor.js +0 -176
package/dist/openclaw-client.js
CHANGED
|
@@ -3,8 +3,21 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
4
4
|
import { basename, extname, join, resolve, sep } from "path";
|
|
5
5
|
import { getBridgeAttachmentsDir, getDataDir } from "./paths.js";
|
|
6
|
-
export const GATEWAY_PROTOCOL_MIN =
|
|
6
|
+
export const GATEWAY_PROTOCOL_MIN = 4;
|
|
7
7
|
export const GATEWAY_PROTOCOL_MAX = 4;
|
|
8
|
+
const REQUIRED_GATEWAY_METHODS = [
|
|
9
|
+
"agent.wait",
|
|
10
|
+
"chat.abort",
|
|
11
|
+
"chat.send",
|
|
12
|
+
"sessions.compact",
|
|
13
|
+
"sessions.create",
|
|
14
|
+
"sessions.delete",
|
|
15
|
+
"sessions.describe",
|
|
16
|
+
"sessions.messages.subscribe",
|
|
17
|
+
"sessions.patch",
|
|
18
|
+
"sessions.reset",
|
|
19
|
+
];
|
|
20
|
+
const REQUIRED_GATEWAY_EVENTS = ["agent", "chat", "session.message"];
|
|
8
21
|
const BRIDGE_ATTACHMENTS_DIR = getBridgeAttachmentsDir();
|
|
9
22
|
const CONTEXT_SYNC_DIR = join(getDataDir(), "context-sync");
|
|
10
23
|
const FEISHU_DOCS_DIR = join(getDataDir(), "feishu-docs");
|
|
@@ -28,11 +41,14 @@ export class OpenClawClient {
|
|
|
28
41
|
/** Callbacks for tool events (verbose mode) */
|
|
29
42
|
toolEventCallbacks = new Map();
|
|
30
43
|
lastToolNamesByItemId = new Map();
|
|
44
|
+
/** Deduplicates 2026.8 canonical `agent.stream=tool` events against legacy
|
|
45
|
+
* `stream=item` mirrors when a runtime emits both representations. */
|
|
46
|
+
recentToolEventKeys = new Map();
|
|
31
47
|
sessionMessageCallbacks = new Map();
|
|
32
48
|
progressCallbacks = new Map();
|
|
33
|
-
/** Per-session steered messages awaiting consumption confirmation.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
49
|
+
/** Per-session steered messages awaiting consumption confirmation. Each
|
|
50
|
+
* accepted message is retained independently so identical/overlapping text
|
|
51
|
+
* cannot overwrite another correlation. */
|
|
36
52
|
pendingSteerTexts = new Map();
|
|
37
53
|
/** Sessions where verbose mode should deliver visible transcript text even when mixed with tool blocks. */
|
|
38
54
|
verboseTranscriptSessions = new Set();
|
|
@@ -45,9 +61,11 @@ export class OpenClawClient {
|
|
|
45
61
|
/** Session keys whose transcript/session.message updates are currently suppressed. */
|
|
46
62
|
suppressedSessions = new Set();
|
|
47
63
|
suppressedSessionTimers = new Map();
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
/** Run ids whose final delivery is owned by chatSend/collectReply. Delivery
|
|
65
|
+
* ownership must be run-scoped: a concurrent queueMode=steer admission may
|
|
66
|
+
* fall through to a separate ordinary run in the same session. */
|
|
67
|
+
ownedDeliveryRuns = new Set();
|
|
68
|
+
ownedDeliveryRunTimers = new Map();
|
|
51
69
|
/** Session keys whose proactive messages must be dropped by the bridge (e.g. discussion scheduler owns delivery). */
|
|
52
70
|
mutedProactiveSessions = new Set();
|
|
53
71
|
mutedProactiveSessionCounts = new Map();
|
|
@@ -105,19 +123,47 @@ export class OpenClawClient {
|
|
|
105
123
|
},
|
|
106
124
|
role: "operator",
|
|
107
125
|
scopes: ["operator.read", "operator.write", "operator.admin"],
|
|
126
|
+
caps: ["tool-events"],
|
|
108
127
|
auth: { token: this.config.token },
|
|
109
128
|
userAgent: "openclaw-lark-multi-agent/1.0.0",
|
|
110
129
|
},
|
|
111
130
|
}));
|
|
112
131
|
}
|
|
113
132
|
else if (frame.type === "res" && frame.ok && frame.payload?.type === "hello-ok") {
|
|
133
|
+
const negotiatedProtocol = Number(frame.payload.protocol);
|
|
134
|
+
if (negotiatedProtocol !== GATEWAY_PROTOCOL_MAX) {
|
|
135
|
+
reject(new Error(`Unsupported Gateway protocol ${frame.payload.protocol}; LMA requires protocol ${GATEWAY_PROTOCOL_MAX}`));
|
|
136
|
+
this.ws?.close();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const advertisedMethods = Array.isArray(frame.payload.features?.methods)
|
|
140
|
+
? new Set(frame.payload.features.methods)
|
|
141
|
+
: null;
|
|
142
|
+
if (advertisedMethods) {
|
|
143
|
+
const missing = REQUIRED_GATEWAY_METHODS.filter((method) => !advertisedMethods.has(method));
|
|
144
|
+
if (missing.length > 0) {
|
|
145
|
+
reject(new Error(`Gateway is missing required methods: ${missing.join(", ")}`));
|
|
146
|
+
this.ws?.close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const advertisedEvents = Array.isArray(frame.payload.features?.events)
|
|
151
|
+
? new Set(frame.payload.features.events)
|
|
152
|
+
: null;
|
|
153
|
+
if (advertisedEvents) {
|
|
154
|
+
const missing = REQUIRED_GATEWAY_EVENTS.filter((event) => !advertisedEvents.has(event));
|
|
155
|
+
if (missing.length > 0) {
|
|
156
|
+
reject(new Error(`Gateway is missing required events: ${missing.join(", ")}`));
|
|
157
|
+
this.ws?.close();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
114
161
|
handshakeDone = true;
|
|
115
162
|
this.connected = true;
|
|
116
163
|
console.log("[OpenClaw] Connected to Gateway WS");
|
|
117
164
|
// Re-subscribe all previously subscribed sessions
|
|
118
165
|
for (const key of this.subscribedKeys) {
|
|
119
|
-
this.rpc("sessions.messages.subscribe", { key }).catch(() => { });
|
|
120
|
-
this.rpc("sessions.messages.subscribe", { key: `agent:main:${key}` }).catch(() => { });
|
|
166
|
+
this.rpc("sessions.messages.subscribe", { key: this.canonicalSessionKey(key) }).catch(() => { });
|
|
121
167
|
}
|
|
122
168
|
resolve();
|
|
123
169
|
}
|
|
@@ -232,7 +278,7 @@ export class OpenClawClient {
|
|
|
232
278
|
data: { deltaText: visibleAssistantText, delta: visibleAssistantText, replace: true },
|
|
233
279
|
});
|
|
234
280
|
}
|
|
235
|
-
this.handleProactiveSessionMessage(rawKey, msg);
|
|
281
|
+
this.handleProactiveSessionMessage(rawKey, msg, frame.payload.runId);
|
|
236
282
|
// Tool calls in assistant messages — skip, using agent item events instead
|
|
237
283
|
// (session.message toolCall events are batched, not real-time)
|
|
238
284
|
}
|
|
@@ -261,15 +307,49 @@ export class OpenClawClient {
|
|
|
261
307
|
this.clearVerboseAssistantState(rawKey);
|
|
262
308
|
}
|
|
263
309
|
if (data.kind === "tool" && data.name && (data.phase === "start" || data.phase === "end" || data.phase === "error")) {
|
|
264
|
-
const phase = data.phase
|
|
265
|
-
const
|
|
310
|
+
const phase = data.phase;
|
|
311
|
+
const eventId = String(data.toolCallId || data.itemId || data.id || data.name);
|
|
312
|
+
if (this.claimToolEvent(rawKey, eventId, phase)) {
|
|
313
|
+
const meta = data.meta || data.input || data.args || "";
|
|
314
|
+
const output = data.output || data.result || data.error || "";
|
|
315
|
+
if (toolCb)
|
|
316
|
+
toolCb(`${data.name} ${phase}`.trim(), String(meta || ""), String(output || ""));
|
|
317
|
+
const progressCb = this.progressCallbacks.get(rawKey) || this.progressCallbacks.get(shortKey);
|
|
318
|
+
if (progressCb) {
|
|
319
|
+
const detail = phase === "start" ? String(meta || "") : String(output || meta || "");
|
|
320
|
+
void progressCb({ kind: "tool", phase, name: String(data.name), text: `${data.name} ${phase}${detail ? `: ${detail}` : ""}` });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// OpenClaw 2026.8 canonical tool stream. Some runtimes also emit the
|
|
326
|
+
// legacy item mirror, so claimToolEvent suppresses duplicate start/end.
|
|
327
|
+
if (frame.event === "agent" && frame.payload?.stream === "tool") {
|
|
328
|
+
const data = frame.payload.data || {};
|
|
329
|
+
const rawKey = frame.payload.sessionKey || "";
|
|
330
|
+
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
331
|
+
const rawPhase = String(data.phase || "");
|
|
332
|
+
const phase = rawPhase === "start"
|
|
333
|
+
? "start"
|
|
334
|
+
: rawPhase === "result"
|
|
335
|
+
? (data.isError || data.error ? "error" : "end")
|
|
336
|
+
: undefined;
|
|
337
|
+
const name = String(data.name || data.toolName || "tool");
|
|
338
|
+
const eventId = String(data.toolCallId || data.itemId || data.id || name);
|
|
339
|
+
if (phase && this.claimToolEvent(rawKey, eventId, phase)) {
|
|
340
|
+
if (phase === "start") {
|
|
341
|
+
this.flushVerboseAssistantState(rawKey);
|
|
342
|
+
this.clearVerboseAssistantState(rawKey);
|
|
343
|
+
}
|
|
344
|
+
const input = data.input || data.args || data.meta || "";
|
|
266
345
|
const output = data.output || data.result || data.error || "";
|
|
346
|
+
const toolCb = this.toolEventCallbacks.get(rawKey) || this.toolEventCallbacks.get(shortKey);
|
|
267
347
|
if (toolCb)
|
|
268
|
-
toolCb(`${
|
|
348
|
+
toolCb(`${name} ${phase}`, String(input || ""), String(output || ""));
|
|
269
349
|
const progressCb = this.progressCallbacks.get(rawKey) || this.progressCallbacks.get(shortKey);
|
|
270
350
|
if (progressCb) {
|
|
271
|
-
const detail = phase === "start" ? String(
|
|
272
|
-
void progressCb({ kind: "tool", phase, name
|
|
351
|
+
const detail = phase === "start" ? String(input || "") : String(output || input || "");
|
|
352
|
+
void progressCb({ kind: "tool", phase, name, text: `${name} ${phase}${detail ? `: ${detail}` : ""}` });
|
|
273
353
|
}
|
|
274
354
|
}
|
|
275
355
|
}
|
|
@@ -371,6 +451,22 @@ export class OpenClawClient {
|
|
|
371
451
|
this.verboseAssistantLastTouched.delete(key);
|
|
372
452
|
}
|
|
373
453
|
}
|
|
454
|
+
claimToolEvent(sessionKey, eventId, phase, now = Date.now()) {
|
|
455
|
+
const normalizedPhase = phase === "error" ? "end" : phase;
|
|
456
|
+
const normalizedId = eventId.replace(/^(?:tool|command):/, "");
|
|
457
|
+
const key = `${this.shortKey(sessionKey)}:${normalizedId}:${normalizedPhase}`;
|
|
458
|
+
const seenAt = this.recentToolEventKeys.get(key);
|
|
459
|
+
if (seenAt !== undefined && now - seenAt < 60_000)
|
|
460
|
+
return false;
|
|
461
|
+
this.recentToolEventKeys.set(key, now);
|
|
462
|
+
if (this.recentToolEventKeys.size > 2000) {
|
|
463
|
+
for (const [candidate, ts] of this.recentToolEventKeys) {
|
|
464
|
+
if (now - ts > 60_000)
|
|
465
|
+
this.recentToolEventKeys.delete(candidate);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
374
470
|
pruneVerboseCaches(now = Date.now()) {
|
|
375
471
|
for (const [key, ts] of this.verboseAssistantLastTouched) {
|
|
376
472
|
if (now - ts > 10 * 60_000) {
|
|
@@ -388,6 +484,10 @@ export class OpenClawClient {
|
|
|
388
484
|
for (const key of Array.from(this.lastToolNamesByItemId.keys()).slice(0, overflow))
|
|
389
485
|
this.lastToolNamesByItemId.delete(key);
|
|
390
486
|
}
|
|
487
|
+
for (const [key, ts] of this.recentToolEventKeys) {
|
|
488
|
+
if (now - ts > 60_000)
|
|
489
|
+
this.recentToolEventKeys.delete(key);
|
|
490
|
+
}
|
|
391
491
|
}
|
|
392
492
|
flushVerboseAssistantState(rawKey) {
|
|
393
493
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
@@ -444,7 +544,7 @@ export class OpenClawClient {
|
|
|
444
544
|
this.flushVerboseAssistantState(rawKey);
|
|
445
545
|
}
|
|
446
546
|
}
|
|
447
|
-
handleProactiveSessionMessage(rawKey, msg) {
|
|
547
|
+
handleProactiveSessionMessage(rawKey, msg, runId) {
|
|
448
548
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
449
549
|
// Proactive assistant text messages. Cron/session-targeted runs often emit
|
|
450
550
|
// structured content arrays rather than a plain string. Extract only visible
|
|
@@ -454,6 +554,10 @@ export class OpenClawClient {
|
|
|
454
554
|
const proactiveText = this.extractVisibleAssistantText(msg, { allowMixedToolText: allowVerboseTranscript });
|
|
455
555
|
if (!proactiveText)
|
|
456
556
|
return false;
|
|
557
|
+
if (runId && this.ownedDeliveryRuns.has(runId)) {
|
|
558
|
+
console.log(`[OpenClaw] Dropping proactive msg for ${shortKey}; run delivery is owned by chatSend`);
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
457
561
|
if (this.mutedProactiveSessions.has(rawKey) || this.mutedProactiveSessions.has(shortKey)) {
|
|
458
562
|
console.log(`[OpenClaw] Dropping proactive msg for ${shortKey}; delivery is owned by the caller`);
|
|
459
563
|
return false;
|
|
@@ -463,8 +567,12 @@ export class OpenClawClient {
|
|
|
463
567
|
return false;
|
|
464
568
|
}
|
|
465
569
|
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
466
|
-
if (cb)
|
|
467
|
-
|
|
570
|
+
if (cb) {
|
|
571
|
+
if (runId)
|
|
572
|
+
cb(proactiveText, { runId });
|
|
573
|
+
else
|
|
574
|
+
cb(proactiveText);
|
|
575
|
+
}
|
|
468
576
|
return Boolean(cb);
|
|
469
577
|
}
|
|
470
578
|
scheduleReconnect() {
|
|
@@ -998,7 +1106,7 @@ export class OpenClawClient {
|
|
|
998
1106
|
: 0;
|
|
999
1107
|
const elapsed = elapsedSeconds > 0 ? `(运行约 ${this.formatDurationSeconds(elapsedSeconds)})` : "";
|
|
1000
1108
|
const phase = timeoutPhase ? `,阶段:${timeoutPhase}` : "";
|
|
1001
|
-
return
|
|
1109
|
+
return `等待 Agent 运行结果超时${elapsed}${phase};底层运行可能仍在继续`;
|
|
1002
1110
|
}
|
|
1003
1111
|
if (rawError)
|
|
1004
1112
|
return rawError;
|
|
@@ -1035,53 +1143,51 @@ export class OpenClawClient {
|
|
|
1035
1143
|
}
|
|
1036
1144
|
// --- Session management ---
|
|
1037
1145
|
async createSession(params) {
|
|
1038
|
-
return this.rpc("sessions.create", params);
|
|
1146
|
+
return this.rpc("sessions.create", { ...params, key: this.canonicalSessionKey(params.key) });
|
|
1039
1147
|
}
|
|
1040
1148
|
async patchSession(params) {
|
|
1041
|
-
return this.rpc("sessions.patch", params, 10000);
|
|
1149
|
+
return this.rpc("sessions.patch", { ...params, key: this.canonicalSessionKey(params.key) }, 10000);
|
|
1042
1150
|
}
|
|
1043
1151
|
async getSessionStatus(key) {
|
|
1044
|
-
return this.rpc("sessions.describe", { key });
|
|
1152
|
+
return this.rpc("sessions.describe", { key: this.canonicalSessionKey(key) });
|
|
1045
1153
|
}
|
|
1046
1154
|
async injectAssistantMessage(params) {
|
|
1047
|
-
return this.rpc("chat.inject", params, 10000);
|
|
1155
|
+
return this.rpc("chat.inject", { ...params, sessionKey: this.canonicalSessionKey(params.sessionKey) }, 10000);
|
|
1048
1156
|
}
|
|
1049
1157
|
/**
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
* - "no_active_run" -> no active run; caller should fall back to normal queueing
|
|
1058
|
-
* - "rejected" -> active run refused the injection (compacting/not streaming)
|
|
1059
|
-
* - "unavailable" -> the lma-steer plugin/method is not available (e.g. not
|
|
1060
|
-
* installed or gateway not restarted); caller falls back.
|
|
1158
|
+
* Submit a mid-run message through OpenClaw's public atomic steer path.
|
|
1159
|
+
* OpenClaw 2026.8 deprecated the synchronous plugin primitive previously used
|
|
1160
|
+
* by `lma-steer`: its boolean only represented immediate eligibility and could
|
|
1161
|
+
* still be rejected asynchronously. `chat.send queueMode=steer` owns admission
|
|
1162
|
+
* and transcript persistence. LMA still treats the RPC result as provisional;
|
|
1163
|
+
* the Feishu reaction flips to Get only after a matching `session.message`
|
|
1164
|
+
* confirms that the input was committed/consumed.
|
|
1061
1165
|
*/
|
|
1062
1166
|
async steer(sessionKey, text, displayText) {
|
|
1167
|
+
const key = this.canonicalSessionKey(sessionKey);
|
|
1063
1168
|
try {
|
|
1064
|
-
const res = await this.rpc("
|
|
1169
|
+
const res = await this.rpc("chat.send", {
|
|
1170
|
+
sessionKey: key,
|
|
1171
|
+
message: text,
|
|
1172
|
+
queueMode: "steer",
|
|
1173
|
+
deliver: false,
|
|
1174
|
+
idempotencyKey: randomUUID(),
|
|
1175
|
+
}, 10000);
|
|
1065
1176
|
const status = res?.status;
|
|
1066
|
-
if (status
|
|
1067
|
-
|
|
1068
|
-
// actually consumes it (a matching sessionUser event on THIS run) we can
|
|
1069
|
-
// render it on the live-status card in correct time order and let the
|
|
1070
|
-
// bridge flip the reaction to Get only after real consumption. Match on
|
|
1071
|
-
// the (possibly wrapped) text actually sent; display the original.
|
|
1072
|
-
if (status === "steered")
|
|
1073
|
-
this.registerPendingSteer(sessionKey, text, displayText);
|
|
1074
|
-
return { status, sessionId: typeof res?.sessionId === "string" ? res.sessionId : undefined };
|
|
1177
|
+
if (status !== "started" && status !== "in_flight" && status !== "ok") {
|
|
1178
|
+
return { status: "unavailable" };
|
|
1075
1179
|
}
|
|
1076
|
-
|
|
1180
|
+
// This records only a provisional accepted input. Actual success is driven
|
|
1181
|
+
// by handleSteerConsumption after the canonical transcript event arrives.
|
|
1182
|
+
const cancelPending = this.registerPendingSteer(key, text, displayText);
|
|
1183
|
+
return {
|
|
1184
|
+
status: "steered",
|
|
1185
|
+
runId: typeof res?.runId === "string" ? res.runId : undefined,
|
|
1186
|
+
cancelPending,
|
|
1187
|
+
};
|
|
1077
1188
|
}
|
|
1078
1189
|
catch (err) {
|
|
1079
|
-
|
|
1080
|
-
// to normal queueing. Never throw into the message path.
|
|
1081
|
-
const msg = err?.message || String(err);
|
|
1082
|
-
if (!/unknown method/i.test(msg)) {
|
|
1083
|
-
console.warn(`[OpenClaw] lma.steer failed for ${sessionKey.slice(-8)}: ${msg}`);
|
|
1084
|
-
}
|
|
1190
|
+
console.warn(`[OpenClaw] native steer failed for ${key.slice(-8)}: ${err?.message || String(err)}`);
|
|
1085
1191
|
return { status: "unavailable" };
|
|
1086
1192
|
}
|
|
1087
1193
|
}
|
|
@@ -1095,29 +1201,71 @@ export class OpenClawClient {
|
|
|
1095
1201
|
* is actually consumed by the active run. The bridge uses this to flip the
|
|
1096
1202
|
* Feishu reaction from "Typing" (awaiting insertion) to "Get" (inserted).
|
|
1097
1203
|
* Return true from the callback to consume/remove it (one-shot). */
|
|
1098
|
-
onSteerConsumed(sessionKey, cb) {
|
|
1204
|
+
onSteerConsumed(sessionKey, matchText, cb) {
|
|
1099
1205
|
const key = this.shortKey(sessionKey);
|
|
1206
|
+
const normalizedMatch = matchText.replace(/\s+/g, " ").trim();
|
|
1100
1207
|
let list = this.steerConsumedCallbacks.get(key);
|
|
1101
1208
|
if (!list) {
|
|
1102
1209
|
list = [];
|
|
1103
1210
|
this.steerConsumedCallbacks.set(key, list);
|
|
1104
1211
|
}
|
|
1105
|
-
|
|
1212
|
+
const entry = { matchText: normalizedMatch, cb };
|
|
1213
|
+
list.push(entry);
|
|
1214
|
+
let active = true;
|
|
1215
|
+
const remove = () => {
|
|
1216
|
+
if (!active)
|
|
1217
|
+
return;
|
|
1218
|
+
active = false;
|
|
1219
|
+
clearTimeout(expiry);
|
|
1220
|
+
const current = this.steerConsumedCallbacks.get(key);
|
|
1221
|
+
if (!current)
|
|
1222
|
+
return;
|
|
1223
|
+
const next = current.filter((candidate) => candidate !== entry);
|
|
1224
|
+
if (next.length > 0)
|
|
1225
|
+
this.steerConsumedCallbacks.set(key, next);
|
|
1226
|
+
else
|
|
1227
|
+
this.steerConsumedCallbacks.delete(key);
|
|
1228
|
+
};
|
|
1229
|
+
const expiry = setTimeout(remove, 30 * 60_000);
|
|
1230
|
+
expiry.unref?.();
|
|
1231
|
+
return remove;
|
|
1106
1232
|
}
|
|
1107
1233
|
shortKey(sessionKey) {
|
|
1108
1234
|
return sessionKey.replace(/^agent:[^:]+:/, "");
|
|
1109
1235
|
}
|
|
1236
|
+
canonicalSessionKey(sessionKey) {
|
|
1237
|
+
return /^agent:[^:]+:/.test(sessionKey) ? sessionKey : `agent:main:${sessionKey}`;
|
|
1238
|
+
}
|
|
1110
1239
|
registerPendingSteer(sessionKey, text, displayText) {
|
|
1111
1240
|
const key = this.shortKey(sessionKey);
|
|
1112
|
-
const
|
|
1113
|
-
if (!
|
|
1114
|
-
return;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1241
|
+
const submittedText = text.replace(/\s+/g, " ").trim();
|
|
1242
|
+
if (!submittedText)
|
|
1243
|
+
return () => { };
|
|
1244
|
+
const displayTextValue = (displayText || text).trim();
|
|
1245
|
+
const queue = this.pendingSteerTexts.get(key) || [];
|
|
1246
|
+
const entry = { submittedText, displayText: displayTextValue };
|
|
1247
|
+
queue.push(entry);
|
|
1248
|
+
this.pendingSteerTexts.set(key, queue);
|
|
1249
|
+
let active = true;
|
|
1250
|
+
const remove = () => {
|
|
1251
|
+
if (!active)
|
|
1252
|
+
return;
|
|
1253
|
+
active = false;
|
|
1254
|
+
clearTimeout(expiry);
|
|
1255
|
+
const current = this.pendingSteerTexts.get(key);
|
|
1256
|
+
if (!current)
|
|
1257
|
+
return;
|
|
1258
|
+
const next = current.filter((candidate) => candidate !== entry);
|
|
1259
|
+
if (next.length > 0)
|
|
1260
|
+
this.pendingSteerTexts.set(key, next);
|
|
1261
|
+
else
|
|
1262
|
+
this.pendingSteerTexts.delete(key);
|
|
1263
|
+
};
|
|
1264
|
+
// Expire only this provisional correlation. A much later transcript must
|
|
1265
|
+
// not consume a stale duplicate while another identical steer is pending.
|
|
1266
|
+
const expiry = setTimeout(remove, 30 * 60_000);
|
|
1267
|
+
expiry.unref?.();
|
|
1268
|
+
return remove;
|
|
1121
1269
|
}
|
|
1122
1270
|
/**
|
|
1123
1271
|
* Called when a sessionUser event is seen for a session. If its text matches a
|
|
@@ -1127,44 +1275,49 @@ export class OpenClawClient {
|
|
|
1127
1275
|
*/
|
|
1128
1276
|
handleSteerConsumption(sessionKey, text) {
|
|
1129
1277
|
const key = this.shortKey(sessionKey);
|
|
1130
|
-
const
|
|
1131
|
-
if (!
|
|
1278
|
+
const queue = this.pendingSteerTexts.get(key);
|
|
1279
|
+
if (!queue || queue.length === 0)
|
|
1132
1280
|
return false;
|
|
1133
1281
|
const norm = (text || "").replace(/\s+/g, " ").trim();
|
|
1134
1282
|
if (!norm)
|
|
1135
1283
|
return false;
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
let
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1284
|
+
// One transcript event consumes exactly one pending steer. Prefer an exact
|
|
1285
|
+
// match, otherwise the longest submitted wrapper contained in the event.
|
|
1286
|
+
let matchedIndex = queue.findIndex((pending) => norm === pending.submittedText);
|
|
1287
|
+
if (matchedIndex < 0) {
|
|
1288
|
+
let matchedLength = -1;
|
|
1289
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1290
|
+
const pending = queue[i];
|
|
1291
|
+
if (norm.includes(pending.submittedText) && pending.submittedText.length > matchedLength) {
|
|
1292
|
+
matchedIndex = i;
|
|
1293
|
+
matchedLength = pending.submittedText.length;
|
|
1294
|
+
}
|
|
1143
1295
|
}
|
|
1144
1296
|
}
|
|
1145
|
-
if (
|
|
1297
|
+
if (matchedIndex < 0)
|
|
1146
1298
|
return false;
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
if (map.size === 0)
|
|
1299
|
+
const [matched] = queue.splice(matchedIndex, 1);
|
|
1300
|
+
if (queue.length === 0)
|
|
1150
1301
|
this.pendingSteerTexts.delete(key);
|
|
1151
1302
|
const progressCb = this.progressCallbacks.get(key) || this.progressCallbacks.get(`agent:main:${key}`);
|
|
1152
1303
|
if (progressCb)
|
|
1153
|
-
void progressCb({ kind: "steer", text:
|
|
1304
|
+
void progressCb({ kind: "steer", text: matched.displayText });
|
|
1154
1305
|
const list = this.steerConsumedCallbacks.get(key);
|
|
1155
1306
|
if (list && list.length > 0) {
|
|
1156
|
-
//
|
|
1157
|
-
|
|
1307
|
+
// Match and remove exactly one observer for this consumed steer. This is
|
|
1308
|
+
// important for duplicate text: one session.message must acknowledge one
|
|
1309
|
+
// Feishu message, never all identical messages at once.
|
|
1310
|
+
let callbackIndex = list.findIndex((entry) => entry.matchText === matched.submittedText);
|
|
1311
|
+
if (callbackIndex < 0)
|
|
1312
|
+
callbackIndex = list.findIndex((entry) => norm.includes(entry.matchText));
|
|
1313
|
+
if (callbackIndex >= 0) {
|
|
1314
|
+
const [entry] = list.splice(callbackIndex, 1);
|
|
1158
1315
|
try {
|
|
1159
|
-
|
|
1316
|
+
entry.cb(matched.submittedText);
|
|
1160
1317
|
}
|
|
1161
|
-
catch {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
});
|
|
1165
|
-
if (remaining.length > 0)
|
|
1166
|
-
this.steerConsumedCallbacks.set(key, remaining);
|
|
1167
|
-
else
|
|
1318
|
+
catch { /* observer failures are isolated */ }
|
|
1319
|
+
}
|
|
1320
|
+
if (list.length === 0)
|
|
1168
1321
|
this.steerConsumedCallbacks.delete(key);
|
|
1169
1322
|
}
|
|
1170
1323
|
return true;
|
|
@@ -1173,41 +1326,57 @@ export class OpenClawClient {
|
|
|
1173
1326
|
* Get session info (model, tokens, etc.) for status display.
|
|
1174
1327
|
*/
|
|
1175
1328
|
async getSessionInfo(sessionKey) {
|
|
1176
|
-
return this.rpc("sessions.describe", { key: sessionKey });
|
|
1329
|
+
return this.rpc("sessions.describe", { key: this.canonicalSessionKey(sessionKey) });
|
|
1177
1330
|
}
|
|
1178
1331
|
/**
|
|
1179
1332
|
* Ensure session is using the expected model. If not, patch it back.
|
|
1180
1333
|
* Returns true if a correction was made.
|
|
1181
1334
|
*/
|
|
1182
1335
|
async ensureModel(sessionKey, expectedModel) {
|
|
1336
|
+
const info = await this.getSessionInfo(sessionKey).catch(() => null);
|
|
1337
|
+
const session = info?.session;
|
|
1338
|
+
const modelId = typeof session?.model === "string" ? session.model.trim() : "";
|
|
1339
|
+
const provider = typeof session?.modelProvider === "string"
|
|
1340
|
+
? session.modelProvider.trim()
|
|
1341
|
+
: typeof session?.provider === "string" ? session.provider.trim() : "";
|
|
1342
|
+
const currentModel = modelId.includes("/") ? modelId : provider && modelId ? `${provider}/${modelId}` : modelId;
|
|
1343
|
+
if (currentModel === expectedModel)
|
|
1344
|
+
return false;
|
|
1183
1345
|
try {
|
|
1184
|
-
|
|
1185
|
-
// Use short timeout as sessions.patch may not return a response
|
|
1186
|
-
await this.patchSession({ key: sessionKey, model: expectedModel }).catch(() => { });
|
|
1187
|
-
// Also try with full key prefix
|
|
1188
|
-
await this.patchSession({ key: `agent:main:${sessionKey}`, model: expectedModel }).catch(() => { });
|
|
1189
|
-
console.log(`[OpenClaw] Model ensured: ${sessionKey} → ${expectedModel}`);
|
|
1346
|
+
await this.patchSession({ key: sessionKey, model: expectedModel });
|
|
1190
1347
|
}
|
|
1191
|
-
catch (
|
|
1192
|
-
|
|
1348
|
+
catch (firstErr) {
|
|
1349
|
+
// Older gateways occasionally require the fully-qualified key. Do not hide
|
|
1350
|
+
// model-policy/capability errors: retry only when the key itself was not
|
|
1351
|
+
// resolved, otherwise surface the actionable rejection to the caller.
|
|
1352
|
+
const message = firstErr.message || String(firstErr);
|
|
1353
|
+
if (!/session (?:not found|unknown)|unknown session|invalid session key/i.test(message))
|
|
1354
|
+
throw firstErr;
|
|
1355
|
+
const fullKey = sessionKey.startsWith("agent:") ? sessionKey : `agent:main:${sessionKey}`;
|
|
1356
|
+
await this.patchSession({ key: fullKey, model: expectedModel });
|
|
1193
1357
|
}
|
|
1194
|
-
|
|
1358
|
+
console.log(`[OpenClaw] Model ensured: ${sessionKey} → ${expectedModel}`);
|
|
1359
|
+
return Boolean(currentModel && currentModel !== expectedModel);
|
|
1195
1360
|
}
|
|
1196
1361
|
async deleteSession(key, deleteTranscript = true) {
|
|
1197
|
-
return this.rpc("sessions.delete", { key, deleteTranscript });
|
|
1362
|
+
return this.rpc("sessions.delete", { key: this.canonicalSessionKey(key), deleteTranscript });
|
|
1198
1363
|
}
|
|
1199
1364
|
async resetSession(key) {
|
|
1200
1365
|
// sessions.reset may not return a response; use short timeout
|
|
1201
|
-
return this.rpc("sessions.reset", { key }, 5000).catch(() => { });
|
|
1366
|
+
return this.rpc("sessions.reset", { key: this.canonicalSessionKey(key) }, 5000).catch(() => { });
|
|
1202
1367
|
}
|
|
1203
|
-
async compactSession(key) {
|
|
1368
|
+
async compactSession(key, options = {}) {
|
|
1204
1369
|
const release = await this.acquireMaintenanceSlot("sessions.compact");
|
|
1205
1370
|
const startedAt = Date.now();
|
|
1371
|
+
const maxLines = Number.isFinite(options.maxLines)
|
|
1372
|
+
? Math.max(1, Math.floor(options.maxLines))
|
|
1373
|
+
: undefined;
|
|
1206
1374
|
try {
|
|
1207
|
-
return await this.rpc("sessions.compact", { key }, 10 * 60 * 1000);
|
|
1375
|
+
return await this.rpc("sessions.compact", { key: this.canonicalSessionKey(key), ...(maxLines !== undefined ? { maxLines } : {}) }, 10 * 60 * 1000);
|
|
1208
1376
|
}
|
|
1209
1377
|
finally {
|
|
1210
|
-
|
|
1378
|
+
const mode = maxLines === undefined ? "semantic" : `maxLines=${maxLines}`;
|
|
1379
|
+
console.log(`[OpenClaw] sessions.compact (${mode}) finished for ${key} in ${Date.now() - startedAt}ms`);
|
|
1211
1380
|
release();
|
|
1212
1381
|
}
|
|
1213
1382
|
}
|
|
@@ -1217,50 +1386,50 @@ export class OpenClawClient {
|
|
|
1217
1386
|
* deliver=false prevents OpenClaw from auto-posting to channels.
|
|
1218
1387
|
*/
|
|
1219
1388
|
async abortChat(sessionKey, runId) {
|
|
1220
|
-
const key =
|
|
1389
|
+
const key = this.canonicalSessionKey(sessionKey);
|
|
1221
1390
|
// chat.abort supports { sessionKey } with no runId to abort ALL active runs
|
|
1222
1391
|
// for that session. Used by /stop to force-clear a stuck run.
|
|
1223
1392
|
if (!runId) {
|
|
1224
1393
|
// Mark the session so any in-flight collectReply finishes immediately
|
|
1225
1394
|
// instead of treating the cancelled lifecycle as a transient state.
|
|
1226
1395
|
this.forceAbortedSessions.add(key);
|
|
1227
|
-
this.forceAbortedSessions.add(
|
|
1396
|
+
this.forceAbortedSessions.add(this.shortKey(key));
|
|
1228
1397
|
}
|
|
1229
1398
|
const params = runId ? { sessionKey: key, runId } : { sessionKey: key };
|
|
1230
1399
|
return this.rpc("chat.abort", params, 5000).catch(() => { });
|
|
1231
1400
|
}
|
|
1232
1401
|
sessionKeyVariants(key) {
|
|
1233
|
-
const shortKey =
|
|
1234
|
-
return [shortKey,
|
|
1402
|
+
const shortKey = this.shortKey(key);
|
|
1403
|
+
return Array.from(new Set([shortKey, this.canonicalSessionKey(key), key]));
|
|
1235
1404
|
}
|
|
1236
1405
|
trackChatEventSession(sessionKey, state, payload) {
|
|
1237
1406
|
if (!sessionKey || sessionKey === "__default__")
|
|
1238
1407
|
return;
|
|
1239
1408
|
const keys = this.sessionKeyVariants(sessionKey);
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
if (state === "final" || state === "error" || state === "aborted")
|
|
1246
|
-
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
1409
|
+
const runId = typeof payload?.runId === "string" ? payload.runId : "";
|
|
1410
|
+
if (runId && this.ownedDeliveryRuns.has(runId)) {
|
|
1411
|
+
// collectReply owns this exact run. Do not suppress the whole session:
|
|
1412
|
+
// queueMode=steer may concurrently create another ordinary run that must
|
|
1413
|
+
// still reach the proactive callback.
|
|
1247
1414
|
return;
|
|
1248
1415
|
}
|
|
1249
|
-
// External WebChat/Control UI
|
|
1250
|
-
// streaming
|
|
1251
|
-
// delivered through the proactive callback after it is committed.
|
|
1416
|
+
// External WebChat/Control UI (including a steer admission that became a
|
|
1417
|
+
// separate run): hide streaming mirrors, then emit its final exactly once.
|
|
1252
1418
|
if (state === "delta") {
|
|
1253
1419
|
this.suppressSessionKeys(keys);
|
|
1254
1420
|
}
|
|
1255
1421
|
else if (state === "final") {
|
|
1256
1422
|
const text = this.extractTextFromChatMessage(payload?.message);
|
|
1257
1423
|
if (text)
|
|
1258
|
-
this.emitProactiveForSession(sessionKey, text);
|
|
1259
|
-
// Keep transcript mirrors suppressed briefly; chat final already emitted
|
|
1260
|
-
// the user-visible result for external WebChat/Control UI turns.
|
|
1424
|
+
this.emitProactiveForSession(sessionKey, text, runId);
|
|
1261
1425
|
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
1262
1426
|
}
|
|
1263
1427
|
else if (state === "error" || state === "aborted") {
|
|
1428
|
+
const detail = String(payload?.errorMessage || payload?.error || payload?.reason || "").trim();
|
|
1429
|
+
const text = state === "error"
|
|
1430
|
+
? `⚠️ Agent run failed${detail ? `: ${detail}` : ""}`
|
|
1431
|
+
: `⚠️ Agent run was aborted${detail ? `: ${detail}` : ""}`;
|
|
1432
|
+
this.emitProactiveForSession(sessionKey, text, runId, "run_error");
|
|
1264
1433
|
this.releaseSuppressedSessionKeysAfter(keys, 0);
|
|
1265
1434
|
}
|
|
1266
1435
|
}
|
|
@@ -1268,12 +1437,15 @@ export class OpenClawClient {
|
|
|
1268
1437
|
const parts = Array.isArray(message?.content) ? message.content : [];
|
|
1269
1438
|
return parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
|
|
1270
1439
|
}
|
|
1271
|
-
emitProactiveForSession(sessionKey, text) {
|
|
1440
|
+
emitProactiveForSession(sessionKey, text, runId, sourceType) {
|
|
1272
1441
|
const [shortKey, fullKey] = this.sessionKeyVariants(sessionKey);
|
|
1273
1442
|
const cb = this.sessionMessageCallbacks.get(fullKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
1274
1443
|
if (!cb)
|
|
1275
1444
|
return false;
|
|
1276
|
-
|
|
1445
|
+
if (runId || sourceType)
|
|
1446
|
+
cb(text, { ...(runId ? { runId } : {}), ...(sourceType ? { sourceType } : {}) });
|
|
1447
|
+
else
|
|
1448
|
+
cb(text);
|
|
1277
1449
|
return true;
|
|
1278
1450
|
}
|
|
1279
1451
|
suppressSessionKeys(keys) {
|
|
@@ -1297,30 +1469,27 @@ export class OpenClawClient {
|
|
|
1297
1469
|
this.suppressedSessionTimers.set(key, timer);
|
|
1298
1470
|
}
|
|
1299
1471
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
releaseOwnedDeliverySessionKeysAfter(keys, delayMs) {
|
|
1310
|
-
for (const key of keys) {
|
|
1311
|
-
const oldTimer = this.ownedDeliverySessionTimers.get(key);
|
|
1312
|
-
if (oldTimer)
|
|
1313
|
-
clearTimeout(oldTimer);
|
|
1314
|
-
const timer = setTimeout(() => {
|
|
1315
|
-
this.ownedDeliverySessions.delete(key);
|
|
1316
|
-
this.ownedDeliverySessionTimers.delete(key);
|
|
1317
|
-
}, delayMs);
|
|
1318
|
-
this.ownedDeliverySessionTimers.set(key, timer);
|
|
1319
|
-
}
|
|
1472
|
+
ownDeliveryRun(runId) {
|
|
1473
|
+
if (!runId)
|
|
1474
|
+
return;
|
|
1475
|
+
const timer = this.ownedDeliveryRunTimers.get(runId);
|
|
1476
|
+
if (timer)
|
|
1477
|
+
clearTimeout(timer);
|
|
1478
|
+
this.ownedDeliveryRunTimers.delete(runId);
|
|
1479
|
+
this.ownedDeliveryRuns.add(runId);
|
|
1320
1480
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1481
|
+
releaseOwnedDeliveryRunAfter(runId, delayMs) {
|
|
1482
|
+
if (!runId)
|
|
1483
|
+
return;
|
|
1484
|
+
const oldTimer = this.ownedDeliveryRunTimers.get(runId);
|
|
1485
|
+
if (oldTimer)
|
|
1486
|
+
clearTimeout(oldTimer);
|
|
1487
|
+
const timer = setTimeout(() => {
|
|
1488
|
+
this.ownedDeliveryRuns.delete(runId);
|
|
1489
|
+
this.ownedDeliveryRunTimers.delete(runId);
|
|
1490
|
+
}, delayMs);
|
|
1491
|
+
timer.unref?.();
|
|
1492
|
+
this.ownedDeliveryRunTimers.set(runId, timer);
|
|
1324
1493
|
}
|
|
1325
1494
|
addMutedProactiveKey(key) {
|
|
1326
1495
|
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
@@ -1395,12 +1564,10 @@ export class OpenClawClient {
|
|
|
1395
1564
|
next();
|
|
1396
1565
|
}
|
|
1397
1566
|
async chatSend(params) {
|
|
1398
|
-
const sk = params.sessionKey;
|
|
1399
|
-
const fullSessionKey =
|
|
1400
|
-
const suppressedKeys = [sk, fullSessionKey];
|
|
1401
|
-
this.suppressSessionKeys(suppressedKeys);
|
|
1402
|
-
this.ownDeliverySessionKeys(suppressedKeys);
|
|
1567
|
+
const sk = this.shortKey(params.sessionKey);
|
|
1568
|
+
const fullSessionKey = this.canonicalSessionKey(params.sessionKey);
|
|
1403
1569
|
this.clearVerboseAssistantState(sk);
|
|
1570
|
+
let ownedRunId = "";
|
|
1404
1571
|
try {
|
|
1405
1572
|
// Drop stale buffered events for this session before starting a new run.
|
|
1406
1573
|
// This prevents an old final text (e.g. previous "ok") from being consumed by
|
|
@@ -1413,7 +1580,7 @@ export class OpenClawClient {
|
|
|
1413
1580
|
try {
|
|
1414
1581
|
await params.onSendAttempt?.();
|
|
1415
1582
|
result = await this.rpc("chat.send", {
|
|
1416
|
-
sessionKey: sk,
|
|
1583
|
+
sessionKey: this.canonicalSessionKey(sk),
|
|
1417
1584
|
message: params.message,
|
|
1418
1585
|
attachments: params.attachments,
|
|
1419
1586
|
deliver: params.deliver ?? false,
|
|
@@ -1423,6 +1590,8 @@ export class OpenClawClient {
|
|
|
1423
1590
|
finally {
|
|
1424
1591
|
releaseChatSendSlot();
|
|
1425
1592
|
}
|
|
1593
|
+
ownedRunId = typeof result?.runId === "string" ? result.runId : "";
|
|
1594
|
+
this.ownDeliveryRun(ownedRunId);
|
|
1426
1595
|
console.log(`[OpenClaw] chat.send runId: ${result.runId} (rpc=${Date.now() - sendStartedAt}ms, attachments=${params.attachments?.length || 0})`);
|
|
1427
1596
|
await params.onSubmitted?.(result.runId);
|
|
1428
1597
|
if (params.onProgress) {
|
|
@@ -1439,8 +1608,7 @@ export class OpenClawClient {
|
|
|
1439
1608
|
// are unaffected because they do not go through chatSend.
|
|
1440
1609
|
this.progressCallbacks.delete(sk);
|
|
1441
1610
|
this.progressCallbacks.delete(fullSessionKey);
|
|
1442
|
-
this.
|
|
1443
|
-
this.releaseOwnedDeliverySessionKeysAfter(suppressedKeys, 30000);
|
|
1611
|
+
this.releaseOwnedDeliveryRunAfter(ownedRunId, 30000);
|
|
1444
1612
|
}
|
|
1445
1613
|
}
|
|
1446
1614
|
shouldInjectBridgeAttachmentHint(text) {
|
|
@@ -1630,9 +1798,9 @@ export class OpenClawClient {
|
|
|
1630
1798
|
this.suppressedSessions.clear();
|
|
1631
1799
|
this.mutedProactiveSessions.clear();
|
|
1632
1800
|
this.mutedProactiveSessionCounts.clear();
|
|
1633
|
-
for (const timer of this.
|
|
1801
|
+
for (const timer of this.ownedDeliveryRunTimers.values())
|
|
1634
1802
|
clearTimeout(timer);
|
|
1635
|
-
this.
|
|
1803
|
+
this.ownedDeliveryRunTimers.clear();
|
|
1636
1804
|
for (const timer of this.verboseAssistantTimers.values())
|
|
1637
1805
|
clearTimeout(timer);
|
|
1638
1806
|
this.verboseAssistantTimers.clear();
|
|
@@ -1640,7 +1808,10 @@ export class OpenClawClient {
|
|
|
1640
1808
|
this.verboseAssistantSent.clear();
|
|
1641
1809
|
this.verboseAssistantLastTouched.clear();
|
|
1642
1810
|
this.lastToolNamesByItemId.clear();
|
|
1643
|
-
this.
|
|
1811
|
+
this.recentToolEventKeys.clear();
|
|
1812
|
+
this.pendingSteerTexts.clear();
|
|
1813
|
+
this.steerConsumedCallbacks.clear();
|
|
1814
|
+
this.ownedDeliveryRuns.clear();
|
|
1644
1815
|
if (this.ws) {
|
|
1645
1816
|
this.ws.close();
|
|
1646
1817
|
this.ws = null;
|
|
@@ -1658,9 +1829,7 @@ export class OpenClawClient {
|
|
|
1658
1829
|
this.sessionMessageCallbacks.set(`agent:main:${sessionKey}`, onMessage);
|
|
1659
1830
|
this.subscribedKeys.add(sessionKey);
|
|
1660
1831
|
try {
|
|
1661
|
-
|
|
1662
|
-
await this.rpc("sessions.messages.subscribe", { key: sessionKey }).catch(() => { });
|
|
1663
|
-
await this.rpc("sessions.messages.subscribe", { key: `agent:main:${sessionKey}` }).catch(() => { });
|
|
1832
|
+
await this.rpc("sessions.messages.subscribe", { key: this.canonicalSessionKey(sessionKey) });
|
|
1664
1833
|
}
|
|
1665
1834
|
catch (err) {
|
|
1666
1835
|
console.warn(`[OpenClaw] Failed to subscribe ${sessionKey}:`, err.message);
|
|
@@ -1685,7 +1854,7 @@ export class OpenClawClient {
|
|
|
1685
1854
|
this.setVerboseTranscriptDelivery(sessionKey, false);
|
|
1686
1855
|
this.subscribedKeys.delete(sessionKey);
|
|
1687
1856
|
try {
|
|
1688
|
-
await this.rpc("sessions.messages.unsubscribe", { key: sessionKey });
|
|
1857
|
+
await this.rpc("sessions.messages.unsubscribe", { key: this.canonicalSessionKey(sessionKey) });
|
|
1689
1858
|
}
|
|
1690
1859
|
catch {
|
|
1691
1860
|
// ignore
|