openclaw-lark-multi-agent 1.4.4 → 1.4.6
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 +33 -15
- package/README.zh-CN.md +8 -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 +19 -9
- package/dist/feishu-bot.js +214 -94
- package/dist/openclaw-client.d.ts +35 -25
- package/dist/openclaw-client.js +400 -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,53 @@ 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
|
+
const inputText = this.formatToolValue(meta, String(data.name), "start");
|
|
316
|
+
const outputText = this.formatToolValue(output, String(data.name), phase);
|
|
317
|
+
if (toolCb)
|
|
318
|
+
toolCb(`${data.name} ${phase}`.trim(), inputText, outputText);
|
|
319
|
+
const progressCb = this.progressCallbacks.get(rawKey) || this.progressCallbacks.get(shortKey);
|
|
320
|
+
if (progressCb) {
|
|
321
|
+
const detail = phase === "start" ? inputText : (outputText || inputText);
|
|
322
|
+
void progressCb({ kind: "tool", phase, name: String(data.name), text: `${data.name} ${phase}${detail ? `: ${detail}` : ""}` });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// OpenClaw 2026.8 canonical tool stream. Some runtimes also emit the
|
|
328
|
+
// legacy item mirror, so claimToolEvent suppresses duplicate start/end.
|
|
329
|
+
if (frame.event === "agent" && frame.payload?.stream === "tool") {
|
|
330
|
+
const data = frame.payload.data || {};
|
|
331
|
+
const rawKey = frame.payload.sessionKey || "";
|
|
332
|
+
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
333
|
+
const rawPhase = String(data.phase || "");
|
|
334
|
+
const phase = rawPhase === "start"
|
|
335
|
+
? "start"
|
|
336
|
+
: rawPhase === "result"
|
|
337
|
+
? (data.isError || data.error ? "error" : "end")
|
|
338
|
+
: undefined;
|
|
339
|
+
const name = String(data.name || data.toolName || "tool");
|
|
340
|
+
const eventId = String(data.toolCallId || data.itemId || data.id || name);
|
|
341
|
+
if (phase && this.claimToolEvent(rawKey, eventId, phase)) {
|
|
342
|
+
if (phase === "start") {
|
|
343
|
+
this.flushVerboseAssistantState(rawKey);
|
|
344
|
+
this.clearVerboseAssistantState(rawKey);
|
|
345
|
+
}
|
|
346
|
+
const input = data.input || data.args || data.meta || "";
|
|
266
347
|
const output = data.output || data.result || data.error || "";
|
|
348
|
+
const inputText = this.formatToolValue(input, name, "start");
|
|
349
|
+
const outputText = this.formatToolValue(output, name, phase);
|
|
350
|
+
const toolCb = this.toolEventCallbacks.get(rawKey) || this.toolEventCallbacks.get(shortKey);
|
|
267
351
|
if (toolCb)
|
|
268
|
-
toolCb(`${
|
|
352
|
+
toolCb(`${name} ${phase}`, inputText, outputText);
|
|
269
353
|
const progressCb = this.progressCallbacks.get(rawKey) || this.progressCallbacks.get(shortKey);
|
|
270
354
|
if (progressCb) {
|
|
271
|
-
const detail = phase === "start" ?
|
|
272
|
-
void progressCb({ kind: "tool", phase, name
|
|
355
|
+
const detail = phase === "start" ? inputText : (outputText || inputText);
|
|
356
|
+
void progressCb({ kind: "tool", phase, name, text: `${name} ${phase}${detail ? `: ${detail}` : ""}` });
|
|
273
357
|
}
|
|
274
358
|
}
|
|
275
359
|
}
|
|
@@ -371,6 +455,91 @@ export class OpenClawClient {
|
|
|
371
455
|
this.verboseAssistantLastTouched.delete(key);
|
|
372
456
|
}
|
|
373
457
|
}
|
|
458
|
+
/** Render protocol-4 structured tool args/results without `[object Object]`.
|
|
459
|
+
* Prefer useful, non-secret fields and keep the fallback bounded. */
|
|
460
|
+
formatToolValue(value, toolName, phase) {
|
|
461
|
+
if (value === null || value === undefined || value === "")
|
|
462
|
+
return "";
|
|
463
|
+
if (typeof value === "string")
|
|
464
|
+
return value.trim();
|
|
465
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
|
|
466
|
+
return String(value);
|
|
467
|
+
if (Array.isArray(value)) {
|
|
468
|
+
const items = value.slice(0, 4).map((item) => this.formatToolValue(item, toolName, phase)).filter(Boolean);
|
|
469
|
+
return items.join("; ");
|
|
470
|
+
}
|
|
471
|
+
if (typeof value !== "object")
|
|
472
|
+
return String(value);
|
|
473
|
+
const record = value;
|
|
474
|
+
const read = (key) => this.formatToolValue(record[key], toolName, phase);
|
|
475
|
+
const first = (...keys) => {
|
|
476
|
+
for (const key of keys) {
|
|
477
|
+
if (!(key in record))
|
|
478
|
+
continue;
|
|
479
|
+
const text = read(key);
|
|
480
|
+
if (text)
|
|
481
|
+
return text;
|
|
482
|
+
}
|
|
483
|
+
return "";
|
|
484
|
+
};
|
|
485
|
+
// Tool-specific start summaries put the primary operation first.
|
|
486
|
+
if (phase === "start") {
|
|
487
|
+
if (toolName === "exec") {
|
|
488
|
+
const command = first("command", "cmd", "script");
|
|
489
|
+
const cwd = first("workdir", "cwd");
|
|
490
|
+
if (command)
|
|
491
|
+
return cwd ? `${command} (cwd: ${cwd})` : command;
|
|
492
|
+
}
|
|
493
|
+
if (toolName === "read" || toolName === "write" || toolName === "edit" || toolName === "apply_patch") {
|
|
494
|
+
const path = first("path", "file", "filename");
|
|
495
|
+
if (path)
|
|
496
|
+
return path;
|
|
497
|
+
}
|
|
498
|
+
if (toolName === "web_fetch") {
|
|
499
|
+
const url = first("url");
|
|
500
|
+
if (url)
|
|
501
|
+
return url;
|
|
502
|
+
}
|
|
503
|
+
if (toolName === "web_search") {
|
|
504
|
+
const query = first("query");
|
|
505
|
+
if (query)
|
|
506
|
+
return query;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const primary = phase === "start"
|
|
510
|
+
? first("command", "path", "url", "query", "action", "target", "sessionKey", "key", "name", "description", "text")
|
|
511
|
+
: first("error", "message", "status", "exitCode", "output", "stdout", "stderr", "text", "result");
|
|
512
|
+
if (primary)
|
|
513
|
+
return primary;
|
|
514
|
+
// Safe generic fallback: omit fields that commonly carry credentials or
|
|
515
|
+
// large payloads, then serialize a few scalar fields only.
|
|
516
|
+
const blocked = /token|secret|password|auth|credential|cookie|buffer|content|data|env/i;
|
|
517
|
+
const parts = [];
|
|
518
|
+
for (const [key, raw] of Object.entries(record)) {
|
|
519
|
+
if (blocked.test(key) || raw === null || raw === undefined || typeof raw === "object")
|
|
520
|
+
continue;
|
|
521
|
+
parts.push(`${key}=${String(raw)}`);
|
|
522
|
+
if (parts.length >= 4)
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
return parts.join(", ");
|
|
526
|
+
}
|
|
527
|
+
claimToolEvent(sessionKey, eventId, phase, now = Date.now()) {
|
|
528
|
+
const normalizedPhase = phase === "error" ? "end" : phase;
|
|
529
|
+
const normalizedId = eventId.replace(/^(?:tool|command):/, "");
|
|
530
|
+
const key = `${this.shortKey(sessionKey)}:${normalizedId}:${normalizedPhase}`;
|
|
531
|
+
const seenAt = this.recentToolEventKeys.get(key);
|
|
532
|
+
if (seenAt !== undefined && now - seenAt < 60_000)
|
|
533
|
+
return false;
|
|
534
|
+
this.recentToolEventKeys.set(key, now);
|
|
535
|
+
if (this.recentToolEventKeys.size > 2000) {
|
|
536
|
+
for (const [candidate, ts] of this.recentToolEventKeys) {
|
|
537
|
+
if (now - ts > 60_000)
|
|
538
|
+
this.recentToolEventKeys.delete(candidate);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
374
543
|
pruneVerboseCaches(now = Date.now()) {
|
|
375
544
|
for (const [key, ts] of this.verboseAssistantLastTouched) {
|
|
376
545
|
if (now - ts > 10 * 60_000) {
|
|
@@ -388,6 +557,10 @@ export class OpenClawClient {
|
|
|
388
557
|
for (const key of Array.from(this.lastToolNamesByItemId.keys()).slice(0, overflow))
|
|
389
558
|
this.lastToolNamesByItemId.delete(key);
|
|
390
559
|
}
|
|
560
|
+
for (const [key, ts] of this.recentToolEventKeys) {
|
|
561
|
+
if (now - ts > 60_000)
|
|
562
|
+
this.recentToolEventKeys.delete(key);
|
|
563
|
+
}
|
|
391
564
|
}
|
|
392
565
|
flushVerboseAssistantState(rawKey) {
|
|
393
566
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
@@ -444,7 +617,7 @@ export class OpenClawClient {
|
|
|
444
617
|
this.flushVerboseAssistantState(rawKey);
|
|
445
618
|
}
|
|
446
619
|
}
|
|
447
|
-
handleProactiveSessionMessage(rawKey, msg) {
|
|
620
|
+
handleProactiveSessionMessage(rawKey, msg, runId) {
|
|
448
621
|
const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
|
|
449
622
|
// Proactive assistant text messages. Cron/session-targeted runs often emit
|
|
450
623
|
// structured content arrays rather than a plain string. Extract only visible
|
|
@@ -454,6 +627,10 @@ export class OpenClawClient {
|
|
|
454
627
|
const proactiveText = this.extractVisibleAssistantText(msg, { allowMixedToolText: allowVerboseTranscript });
|
|
455
628
|
if (!proactiveText)
|
|
456
629
|
return false;
|
|
630
|
+
if (runId && this.ownedDeliveryRuns.has(runId)) {
|
|
631
|
+
console.log(`[OpenClaw] Dropping proactive msg for ${shortKey}; run delivery is owned by chatSend`);
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
457
634
|
if (this.mutedProactiveSessions.has(rawKey) || this.mutedProactiveSessions.has(shortKey)) {
|
|
458
635
|
console.log(`[OpenClaw] Dropping proactive msg for ${shortKey}; delivery is owned by the caller`);
|
|
459
636
|
return false;
|
|
@@ -463,8 +640,12 @@ export class OpenClawClient {
|
|
|
463
640
|
return false;
|
|
464
641
|
}
|
|
465
642
|
const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
466
|
-
if (cb)
|
|
467
|
-
|
|
643
|
+
if (cb) {
|
|
644
|
+
if (runId)
|
|
645
|
+
cb(proactiveText, { runId });
|
|
646
|
+
else
|
|
647
|
+
cb(proactiveText);
|
|
648
|
+
}
|
|
468
649
|
return Boolean(cb);
|
|
469
650
|
}
|
|
470
651
|
scheduleReconnect() {
|
|
@@ -998,7 +1179,7 @@ export class OpenClawClient {
|
|
|
998
1179
|
: 0;
|
|
999
1180
|
const elapsed = elapsedSeconds > 0 ? `(运行约 ${this.formatDurationSeconds(elapsedSeconds)})` : "";
|
|
1000
1181
|
const phase = timeoutPhase ? `,阶段:${timeoutPhase}` : "";
|
|
1001
|
-
return
|
|
1182
|
+
return `等待 Agent 运行结果超时${elapsed}${phase};底层运行可能仍在继续`;
|
|
1002
1183
|
}
|
|
1003
1184
|
if (rawError)
|
|
1004
1185
|
return rawError;
|
|
@@ -1035,53 +1216,51 @@ export class OpenClawClient {
|
|
|
1035
1216
|
}
|
|
1036
1217
|
// --- Session management ---
|
|
1037
1218
|
async createSession(params) {
|
|
1038
|
-
return this.rpc("sessions.create", params);
|
|
1219
|
+
return this.rpc("sessions.create", { ...params, key: this.canonicalSessionKey(params.key) });
|
|
1039
1220
|
}
|
|
1040
1221
|
async patchSession(params) {
|
|
1041
|
-
return this.rpc("sessions.patch", params, 10000);
|
|
1222
|
+
return this.rpc("sessions.patch", { ...params, key: this.canonicalSessionKey(params.key) }, 10000);
|
|
1042
1223
|
}
|
|
1043
1224
|
async getSessionStatus(key) {
|
|
1044
|
-
return this.rpc("sessions.describe", { key });
|
|
1225
|
+
return this.rpc("sessions.describe", { key: this.canonicalSessionKey(key) });
|
|
1045
1226
|
}
|
|
1046
1227
|
async injectAssistantMessage(params) {
|
|
1047
|
-
return this.rpc("chat.inject", params, 10000);
|
|
1228
|
+
return this.rpc("chat.inject", { ...params, sessionKey: this.canonicalSessionKey(params.sessionKey) }, 10000);
|
|
1048
1229
|
}
|
|
1049
1230
|
/**
|
|
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.
|
|
1231
|
+
* Submit a mid-run message through OpenClaw's public atomic steer path.
|
|
1232
|
+
* OpenClaw 2026.8 deprecated the synchronous plugin primitive previously used
|
|
1233
|
+
* by `lma-steer`: its boolean only represented immediate eligibility and could
|
|
1234
|
+
* still be rejected asynchronously. `chat.send queueMode=steer` owns admission
|
|
1235
|
+
* and transcript persistence. LMA still treats the RPC result as provisional;
|
|
1236
|
+
* the Feishu reaction flips to Get only after a matching `session.message`
|
|
1237
|
+
* confirms that the input was committed/consumed.
|
|
1061
1238
|
*/
|
|
1062
1239
|
async steer(sessionKey, text, displayText) {
|
|
1240
|
+
const key = this.canonicalSessionKey(sessionKey);
|
|
1063
1241
|
try {
|
|
1064
|
-
const res = await this.rpc("
|
|
1242
|
+
const res = await this.rpc("chat.send", {
|
|
1243
|
+
sessionKey: key,
|
|
1244
|
+
message: text,
|
|
1245
|
+
queueMode: "steer",
|
|
1246
|
+
deliver: false,
|
|
1247
|
+
idempotencyKey: randomUUID(),
|
|
1248
|
+
}, 10000);
|
|
1065
1249
|
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 };
|
|
1250
|
+
if (status !== "started" && status !== "in_flight" && status !== "ok") {
|
|
1251
|
+
return { status: "unavailable" };
|
|
1075
1252
|
}
|
|
1076
|
-
|
|
1253
|
+
// This records only a provisional accepted input. Actual success is driven
|
|
1254
|
+
// by handleSteerConsumption after the canonical transcript event arrives.
|
|
1255
|
+
const cancelPending = this.registerPendingSteer(key, text, displayText);
|
|
1256
|
+
return {
|
|
1257
|
+
status: "steered",
|
|
1258
|
+
runId: typeof res?.runId === "string" ? res.runId : undefined,
|
|
1259
|
+
cancelPending,
|
|
1260
|
+
};
|
|
1077
1261
|
}
|
|
1078
1262
|
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
|
-
}
|
|
1263
|
+
console.warn(`[OpenClaw] native steer failed for ${key.slice(-8)}: ${err?.message || String(err)}`);
|
|
1085
1264
|
return { status: "unavailable" };
|
|
1086
1265
|
}
|
|
1087
1266
|
}
|
|
@@ -1095,29 +1274,71 @@ export class OpenClawClient {
|
|
|
1095
1274
|
* is actually consumed by the active run. The bridge uses this to flip the
|
|
1096
1275
|
* Feishu reaction from "Typing" (awaiting insertion) to "Get" (inserted).
|
|
1097
1276
|
* Return true from the callback to consume/remove it (one-shot). */
|
|
1098
|
-
onSteerConsumed(sessionKey, cb) {
|
|
1277
|
+
onSteerConsumed(sessionKey, matchText, cb) {
|
|
1099
1278
|
const key = this.shortKey(sessionKey);
|
|
1279
|
+
const normalizedMatch = matchText.replace(/\s+/g, " ").trim();
|
|
1100
1280
|
let list = this.steerConsumedCallbacks.get(key);
|
|
1101
1281
|
if (!list) {
|
|
1102
1282
|
list = [];
|
|
1103
1283
|
this.steerConsumedCallbacks.set(key, list);
|
|
1104
1284
|
}
|
|
1105
|
-
|
|
1285
|
+
const entry = { matchText: normalizedMatch, cb };
|
|
1286
|
+
list.push(entry);
|
|
1287
|
+
let active = true;
|
|
1288
|
+
const remove = () => {
|
|
1289
|
+
if (!active)
|
|
1290
|
+
return;
|
|
1291
|
+
active = false;
|
|
1292
|
+
clearTimeout(expiry);
|
|
1293
|
+
const current = this.steerConsumedCallbacks.get(key);
|
|
1294
|
+
if (!current)
|
|
1295
|
+
return;
|
|
1296
|
+
const next = current.filter((candidate) => candidate !== entry);
|
|
1297
|
+
if (next.length > 0)
|
|
1298
|
+
this.steerConsumedCallbacks.set(key, next);
|
|
1299
|
+
else
|
|
1300
|
+
this.steerConsumedCallbacks.delete(key);
|
|
1301
|
+
};
|
|
1302
|
+
const expiry = setTimeout(remove, 30 * 60_000);
|
|
1303
|
+
expiry.unref?.();
|
|
1304
|
+
return remove;
|
|
1106
1305
|
}
|
|
1107
1306
|
shortKey(sessionKey) {
|
|
1108
1307
|
return sessionKey.replace(/^agent:[^:]+:/, "");
|
|
1109
1308
|
}
|
|
1309
|
+
canonicalSessionKey(sessionKey) {
|
|
1310
|
+
return /^agent:[^:]+:/.test(sessionKey) ? sessionKey : `agent:main:${sessionKey}`;
|
|
1311
|
+
}
|
|
1110
1312
|
registerPendingSteer(sessionKey, text, displayText) {
|
|
1111
1313
|
const key = this.shortKey(sessionKey);
|
|
1112
|
-
const
|
|
1113
|
-
if (!
|
|
1114
|
-
return;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1314
|
+
const submittedText = text.replace(/\s+/g, " ").trim();
|
|
1315
|
+
if (!submittedText)
|
|
1316
|
+
return () => { };
|
|
1317
|
+
const displayTextValue = (displayText || text).trim();
|
|
1318
|
+
const queue = this.pendingSteerTexts.get(key) || [];
|
|
1319
|
+
const entry = { submittedText, displayText: displayTextValue };
|
|
1320
|
+
queue.push(entry);
|
|
1321
|
+
this.pendingSteerTexts.set(key, queue);
|
|
1322
|
+
let active = true;
|
|
1323
|
+
const remove = () => {
|
|
1324
|
+
if (!active)
|
|
1325
|
+
return;
|
|
1326
|
+
active = false;
|
|
1327
|
+
clearTimeout(expiry);
|
|
1328
|
+
const current = this.pendingSteerTexts.get(key);
|
|
1329
|
+
if (!current)
|
|
1330
|
+
return;
|
|
1331
|
+
const next = current.filter((candidate) => candidate !== entry);
|
|
1332
|
+
if (next.length > 0)
|
|
1333
|
+
this.pendingSteerTexts.set(key, next);
|
|
1334
|
+
else
|
|
1335
|
+
this.pendingSteerTexts.delete(key);
|
|
1336
|
+
};
|
|
1337
|
+
// Expire only this provisional correlation. A much later transcript must
|
|
1338
|
+
// not consume a stale duplicate while another identical steer is pending.
|
|
1339
|
+
const expiry = setTimeout(remove, 30 * 60_000);
|
|
1340
|
+
expiry.unref?.();
|
|
1341
|
+
return remove;
|
|
1121
1342
|
}
|
|
1122
1343
|
/**
|
|
1123
1344
|
* Called when a sessionUser event is seen for a session. If its text matches a
|
|
@@ -1127,44 +1348,49 @@ export class OpenClawClient {
|
|
|
1127
1348
|
*/
|
|
1128
1349
|
handleSteerConsumption(sessionKey, text) {
|
|
1129
1350
|
const key = this.shortKey(sessionKey);
|
|
1130
|
-
const
|
|
1131
|
-
if (!
|
|
1351
|
+
const queue = this.pendingSteerTexts.get(key);
|
|
1352
|
+
if (!queue || queue.length === 0)
|
|
1132
1353
|
return false;
|
|
1133
1354
|
const norm = (text || "").replace(/\s+/g, " ").trim();
|
|
1134
1355
|
if (!norm)
|
|
1135
1356
|
return false;
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
let
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1357
|
+
// One transcript event consumes exactly one pending steer. Prefer an exact
|
|
1358
|
+
// match, otherwise the longest submitted wrapper contained in the event.
|
|
1359
|
+
let matchedIndex = queue.findIndex((pending) => norm === pending.submittedText);
|
|
1360
|
+
if (matchedIndex < 0) {
|
|
1361
|
+
let matchedLength = -1;
|
|
1362
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1363
|
+
const pending = queue[i];
|
|
1364
|
+
if (norm.includes(pending.submittedText) && pending.submittedText.length > matchedLength) {
|
|
1365
|
+
matchedIndex = i;
|
|
1366
|
+
matchedLength = pending.submittedText.length;
|
|
1367
|
+
}
|
|
1143
1368
|
}
|
|
1144
1369
|
}
|
|
1145
|
-
if (
|
|
1370
|
+
if (matchedIndex < 0)
|
|
1146
1371
|
return false;
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
if (map.size === 0)
|
|
1372
|
+
const [matched] = queue.splice(matchedIndex, 1);
|
|
1373
|
+
if (queue.length === 0)
|
|
1150
1374
|
this.pendingSteerTexts.delete(key);
|
|
1151
1375
|
const progressCb = this.progressCallbacks.get(key) || this.progressCallbacks.get(`agent:main:${key}`);
|
|
1152
1376
|
if (progressCb)
|
|
1153
|
-
void progressCb({ kind: "steer", text:
|
|
1377
|
+
void progressCb({ kind: "steer", text: matched.displayText });
|
|
1154
1378
|
const list = this.steerConsumedCallbacks.get(key);
|
|
1155
1379
|
if (list && list.length > 0) {
|
|
1156
|
-
//
|
|
1157
|
-
|
|
1380
|
+
// Match and remove exactly one observer for this consumed steer. This is
|
|
1381
|
+
// important for duplicate text: one session.message must acknowledge one
|
|
1382
|
+
// Feishu message, never all identical messages at once.
|
|
1383
|
+
let callbackIndex = list.findIndex((entry) => entry.matchText === matched.submittedText);
|
|
1384
|
+
if (callbackIndex < 0)
|
|
1385
|
+
callbackIndex = list.findIndex((entry) => norm.includes(entry.matchText));
|
|
1386
|
+
if (callbackIndex >= 0) {
|
|
1387
|
+
const [entry] = list.splice(callbackIndex, 1);
|
|
1158
1388
|
try {
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
catch {
|
|
1162
|
-
return true;
|
|
1389
|
+
entry.cb(matched.submittedText);
|
|
1163
1390
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
else
|
|
1391
|
+
catch { /* observer failures are isolated */ }
|
|
1392
|
+
}
|
|
1393
|
+
if (list.length === 0)
|
|
1168
1394
|
this.steerConsumedCallbacks.delete(key);
|
|
1169
1395
|
}
|
|
1170
1396
|
return true;
|
|
@@ -1173,41 +1399,57 @@ export class OpenClawClient {
|
|
|
1173
1399
|
* Get session info (model, tokens, etc.) for status display.
|
|
1174
1400
|
*/
|
|
1175
1401
|
async getSessionInfo(sessionKey) {
|
|
1176
|
-
return this.rpc("sessions.describe", { key: sessionKey });
|
|
1402
|
+
return this.rpc("sessions.describe", { key: this.canonicalSessionKey(sessionKey) });
|
|
1177
1403
|
}
|
|
1178
1404
|
/**
|
|
1179
1405
|
* Ensure session is using the expected model. If not, patch it back.
|
|
1180
1406
|
* Returns true if a correction was made.
|
|
1181
1407
|
*/
|
|
1182
1408
|
async ensureModel(sessionKey, expectedModel) {
|
|
1409
|
+
const info = await this.getSessionInfo(sessionKey).catch(() => null);
|
|
1410
|
+
const session = info?.session;
|
|
1411
|
+
const modelId = typeof session?.model === "string" ? session.model.trim() : "";
|
|
1412
|
+
const provider = typeof session?.modelProvider === "string"
|
|
1413
|
+
? session.modelProvider.trim()
|
|
1414
|
+
: typeof session?.provider === "string" ? session.provider.trim() : "";
|
|
1415
|
+
const currentModel = modelId.includes("/") ? modelId : provider && modelId ? `${provider}/${modelId}` : modelId;
|
|
1416
|
+
if (currentModel === expectedModel)
|
|
1417
|
+
return false;
|
|
1183
1418
|
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}`);
|
|
1419
|
+
await this.patchSession({ key: sessionKey, model: expectedModel });
|
|
1190
1420
|
}
|
|
1191
|
-
catch (
|
|
1192
|
-
|
|
1421
|
+
catch (firstErr) {
|
|
1422
|
+
// Older gateways occasionally require the fully-qualified key. Do not hide
|
|
1423
|
+
// model-policy/capability errors: retry only when the key itself was not
|
|
1424
|
+
// resolved, otherwise surface the actionable rejection to the caller.
|
|
1425
|
+
const message = firstErr.message || String(firstErr);
|
|
1426
|
+
if (!/session (?:not found|unknown)|unknown session|invalid session key/i.test(message))
|
|
1427
|
+
throw firstErr;
|
|
1428
|
+
const fullKey = sessionKey.startsWith("agent:") ? sessionKey : `agent:main:${sessionKey}`;
|
|
1429
|
+
await this.patchSession({ key: fullKey, model: expectedModel });
|
|
1193
1430
|
}
|
|
1194
|
-
|
|
1431
|
+
console.log(`[OpenClaw] Model ensured: ${sessionKey} → ${expectedModel}`);
|
|
1432
|
+
return Boolean(currentModel && currentModel !== expectedModel);
|
|
1195
1433
|
}
|
|
1196
1434
|
async deleteSession(key, deleteTranscript = true) {
|
|
1197
|
-
return this.rpc("sessions.delete", { key, deleteTranscript });
|
|
1435
|
+
return this.rpc("sessions.delete", { key: this.canonicalSessionKey(key), deleteTranscript });
|
|
1198
1436
|
}
|
|
1199
1437
|
async resetSession(key) {
|
|
1200
1438
|
// sessions.reset may not return a response; use short timeout
|
|
1201
|
-
return this.rpc("sessions.reset", { key }, 5000).catch(() => { });
|
|
1439
|
+
return this.rpc("sessions.reset", { key: this.canonicalSessionKey(key) }, 5000).catch(() => { });
|
|
1202
1440
|
}
|
|
1203
|
-
async compactSession(key) {
|
|
1441
|
+
async compactSession(key, options = {}) {
|
|
1204
1442
|
const release = await this.acquireMaintenanceSlot("sessions.compact");
|
|
1205
1443
|
const startedAt = Date.now();
|
|
1444
|
+
const maxLines = Number.isFinite(options.maxLines)
|
|
1445
|
+
? Math.max(1, Math.floor(options.maxLines))
|
|
1446
|
+
: undefined;
|
|
1206
1447
|
try {
|
|
1207
|
-
return await this.rpc("sessions.compact", { key }, 10 * 60 * 1000);
|
|
1448
|
+
return await this.rpc("sessions.compact", { key: this.canonicalSessionKey(key), ...(maxLines !== undefined ? { maxLines } : {}) }, 10 * 60 * 1000);
|
|
1208
1449
|
}
|
|
1209
1450
|
finally {
|
|
1210
|
-
|
|
1451
|
+
const mode = maxLines === undefined ? "semantic" : `maxLines=${maxLines}`;
|
|
1452
|
+
console.log(`[OpenClaw] sessions.compact (${mode}) finished for ${key} in ${Date.now() - startedAt}ms`);
|
|
1211
1453
|
release();
|
|
1212
1454
|
}
|
|
1213
1455
|
}
|
|
@@ -1217,50 +1459,50 @@ export class OpenClawClient {
|
|
|
1217
1459
|
* deliver=false prevents OpenClaw from auto-posting to channels.
|
|
1218
1460
|
*/
|
|
1219
1461
|
async abortChat(sessionKey, runId) {
|
|
1220
|
-
const key =
|
|
1462
|
+
const key = this.canonicalSessionKey(sessionKey);
|
|
1221
1463
|
// chat.abort supports { sessionKey } with no runId to abort ALL active runs
|
|
1222
1464
|
// for that session. Used by /stop to force-clear a stuck run.
|
|
1223
1465
|
if (!runId) {
|
|
1224
1466
|
// Mark the session so any in-flight collectReply finishes immediately
|
|
1225
1467
|
// instead of treating the cancelled lifecycle as a transient state.
|
|
1226
1468
|
this.forceAbortedSessions.add(key);
|
|
1227
|
-
this.forceAbortedSessions.add(
|
|
1469
|
+
this.forceAbortedSessions.add(this.shortKey(key));
|
|
1228
1470
|
}
|
|
1229
1471
|
const params = runId ? { sessionKey: key, runId } : { sessionKey: key };
|
|
1230
1472
|
return this.rpc("chat.abort", params, 5000).catch(() => { });
|
|
1231
1473
|
}
|
|
1232
1474
|
sessionKeyVariants(key) {
|
|
1233
|
-
const shortKey =
|
|
1234
|
-
return [shortKey,
|
|
1475
|
+
const shortKey = this.shortKey(key);
|
|
1476
|
+
return Array.from(new Set([shortKey, this.canonicalSessionKey(key), key]));
|
|
1235
1477
|
}
|
|
1236
1478
|
trackChatEventSession(sessionKey, state, payload) {
|
|
1237
1479
|
if (!sessionKey || sessionKey === "__default__")
|
|
1238
1480
|
return;
|
|
1239
1481
|
const keys = this.sessionKeyVariants(sessionKey);
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
if (state === "final" || state === "error" || state === "aborted")
|
|
1246
|
-
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
1482
|
+
const runId = typeof payload?.runId === "string" ? payload.runId : "";
|
|
1483
|
+
if (runId && this.ownedDeliveryRuns.has(runId)) {
|
|
1484
|
+
// collectReply owns this exact run. Do not suppress the whole session:
|
|
1485
|
+
// queueMode=steer may concurrently create another ordinary run that must
|
|
1486
|
+
// still reach the proactive callback.
|
|
1247
1487
|
return;
|
|
1248
1488
|
}
|
|
1249
|
-
// External WebChat/Control UI
|
|
1250
|
-
// streaming
|
|
1251
|
-
// delivered through the proactive callback after it is committed.
|
|
1489
|
+
// External WebChat/Control UI (including a steer admission that became a
|
|
1490
|
+
// separate run): hide streaming mirrors, then emit its final exactly once.
|
|
1252
1491
|
if (state === "delta") {
|
|
1253
1492
|
this.suppressSessionKeys(keys);
|
|
1254
1493
|
}
|
|
1255
1494
|
else if (state === "final") {
|
|
1256
1495
|
const text = this.extractTextFromChatMessage(payload?.message);
|
|
1257
1496
|
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.
|
|
1497
|
+
this.emitProactiveForSession(sessionKey, text, runId);
|
|
1261
1498
|
this.releaseSuppressedSessionKeysAfter(keys, 30000);
|
|
1262
1499
|
}
|
|
1263
1500
|
else if (state === "error" || state === "aborted") {
|
|
1501
|
+
const detail = String(payload?.errorMessage || payload?.error || payload?.reason || "").trim();
|
|
1502
|
+
const text = state === "error"
|
|
1503
|
+
? `⚠️ Agent run failed${detail ? `: ${detail}` : ""}`
|
|
1504
|
+
: `⚠️ Agent run was aborted${detail ? `: ${detail}` : ""}`;
|
|
1505
|
+
this.emitProactiveForSession(sessionKey, text, runId, "run_error");
|
|
1264
1506
|
this.releaseSuppressedSessionKeysAfter(keys, 0);
|
|
1265
1507
|
}
|
|
1266
1508
|
}
|
|
@@ -1268,12 +1510,15 @@ export class OpenClawClient {
|
|
|
1268
1510
|
const parts = Array.isArray(message?.content) ? message.content : [];
|
|
1269
1511
|
return parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
|
|
1270
1512
|
}
|
|
1271
|
-
emitProactiveForSession(sessionKey, text) {
|
|
1513
|
+
emitProactiveForSession(sessionKey, text, runId, sourceType) {
|
|
1272
1514
|
const [shortKey, fullKey] = this.sessionKeyVariants(sessionKey);
|
|
1273
1515
|
const cb = this.sessionMessageCallbacks.get(fullKey) || this.sessionMessageCallbacks.get(shortKey);
|
|
1274
1516
|
if (!cb)
|
|
1275
1517
|
return false;
|
|
1276
|
-
|
|
1518
|
+
if (runId || sourceType)
|
|
1519
|
+
cb(text, { ...(runId ? { runId } : {}), ...(sourceType ? { sourceType } : {}) });
|
|
1520
|
+
else
|
|
1521
|
+
cb(text);
|
|
1277
1522
|
return true;
|
|
1278
1523
|
}
|
|
1279
1524
|
suppressSessionKeys(keys) {
|
|
@@ -1297,30 +1542,27 @@ export class OpenClawClient {
|
|
|
1297
1542
|
this.suppressedSessionTimers.set(key, timer);
|
|
1298
1543
|
}
|
|
1299
1544
|
}
|
|
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
|
-
}
|
|
1545
|
+
ownDeliveryRun(runId) {
|
|
1546
|
+
if (!runId)
|
|
1547
|
+
return;
|
|
1548
|
+
const timer = this.ownedDeliveryRunTimers.get(runId);
|
|
1549
|
+
if (timer)
|
|
1550
|
+
clearTimeout(timer);
|
|
1551
|
+
this.ownedDeliveryRunTimers.delete(runId);
|
|
1552
|
+
this.ownedDeliveryRuns.add(runId);
|
|
1320
1553
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1554
|
+
releaseOwnedDeliveryRunAfter(runId, delayMs) {
|
|
1555
|
+
if (!runId)
|
|
1556
|
+
return;
|
|
1557
|
+
const oldTimer = this.ownedDeliveryRunTimers.get(runId);
|
|
1558
|
+
if (oldTimer)
|
|
1559
|
+
clearTimeout(oldTimer);
|
|
1560
|
+
const timer = setTimeout(() => {
|
|
1561
|
+
this.ownedDeliveryRuns.delete(runId);
|
|
1562
|
+
this.ownedDeliveryRunTimers.delete(runId);
|
|
1563
|
+
}, delayMs);
|
|
1564
|
+
timer.unref?.();
|
|
1565
|
+
this.ownedDeliveryRunTimers.set(runId, timer);
|
|
1324
1566
|
}
|
|
1325
1567
|
addMutedProactiveKey(key) {
|
|
1326
1568
|
const count = this.mutedProactiveSessionCounts.get(key) || 0;
|
|
@@ -1395,12 +1637,10 @@ export class OpenClawClient {
|
|
|
1395
1637
|
next();
|
|
1396
1638
|
}
|
|
1397
1639
|
async chatSend(params) {
|
|
1398
|
-
const sk = params.sessionKey;
|
|
1399
|
-
const fullSessionKey =
|
|
1400
|
-
const suppressedKeys = [sk, fullSessionKey];
|
|
1401
|
-
this.suppressSessionKeys(suppressedKeys);
|
|
1402
|
-
this.ownDeliverySessionKeys(suppressedKeys);
|
|
1640
|
+
const sk = this.shortKey(params.sessionKey);
|
|
1641
|
+
const fullSessionKey = this.canonicalSessionKey(params.sessionKey);
|
|
1403
1642
|
this.clearVerboseAssistantState(sk);
|
|
1643
|
+
let ownedRunId = "";
|
|
1404
1644
|
try {
|
|
1405
1645
|
// Drop stale buffered events for this session before starting a new run.
|
|
1406
1646
|
// This prevents an old final text (e.g. previous "ok") from being consumed by
|
|
@@ -1413,7 +1653,7 @@ export class OpenClawClient {
|
|
|
1413
1653
|
try {
|
|
1414
1654
|
await params.onSendAttempt?.();
|
|
1415
1655
|
result = await this.rpc("chat.send", {
|
|
1416
|
-
sessionKey: sk,
|
|
1656
|
+
sessionKey: this.canonicalSessionKey(sk),
|
|
1417
1657
|
message: params.message,
|
|
1418
1658
|
attachments: params.attachments,
|
|
1419
1659
|
deliver: params.deliver ?? false,
|
|
@@ -1423,6 +1663,8 @@ export class OpenClawClient {
|
|
|
1423
1663
|
finally {
|
|
1424
1664
|
releaseChatSendSlot();
|
|
1425
1665
|
}
|
|
1666
|
+
ownedRunId = typeof result?.runId === "string" ? result.runId : "";
|
|
1667
|
+
this.ownDeliveryRun(ownedRunId);
|
|
1426
1668
|
console.log(`[OpenClaw] chat.send runId: ${result.runId} (rpc=${Date.now() - sendStartedAt}ms, attachments=${params.attachments?.length || 0})`);
|
|
1427
1669
|
await params.onSubmitted?.(result.runId);
|
|
1428
1670
|
if (params.onProgress) {
|
|
@@ -1439,8 +1681,7 @@ export class OpenClawClient {
|
|
|
1439
1681
|
// are unaffected because they do not go through chatSend.
|
|
1440
1682
|
this.progressCallbacks.delete(sk);
|
|
1441
1683
|
this.progressCallbacks.delete(fullSessionKey);
|
|
1442
|
-
this.
|
|
1443
|
-
this.releaseOwnedDeliverySessionKeysAfter(suppressedKeys, 30000);
|
|
1684
|
+
this.releaseOwnedDeliveryRunAfter(ownedRunId, 30000);
|
|
1444
1685
|
}
|
|
1445
1686
|
}
|
|
1446
1687
|
shouldInjectBridgeAttachmentHint(text) {
|
|
@@ -1630,9 +1871,9 @@ export class OpenClawClient {
|
|
|
1630
1871
|
this.suppressedSessions.clear();
|
|
1631
1872
|
this.mutedProactiveSessions.clear();
|
|
1632
1873
|
this.mutedProactiveSessionCounts.clear();
|
|
1633
|
-
for (const timer of this.
|
|
1874
|
+
for (const timer of this.ownedDeliveryRunTimers.values())
|
|
1634
1875
|
clearTimeout(timer);
|
|
1635
|
-
this.
|
|
1876
|
+
this.ownedDeliveryRunTimers.clear();
|
|
1636
1877
|
for (const timer of this.verboseAssistantTimers.values())
|
|
1637
1878
|
clearTimeout(timer);
|
|
1638
1879
|
this.verboseAssistantTimers.clear();
|
|
@@ -1640,7 +1881,10 @@ export class OpenClawClient {
|
|
|
1640
1881
|
this.verboseAssistantSent.clear();
|
|
1641
1882
|
this.verboseAssistantLastTouched.clear();
|
|
1642
1883
|
this.lastToolNamesByItemId.clear();
|
|
1643
|
-
this.
|
|
1884
|
+
this.recentToolEventKeys.clear();
|
|
1885
|
+
this.pendingSteerTexts.clear();
|
|
1886
|
+
this.steerConsumedCallbacks.clear();
|
|
1887
|
+
this.ownedDeliveryRuns.clear();
|
|
1644
1888
|
if (this.ws) {
|
|
1645
1889
|
this.ws.close();
|
|
1646
1890
|
this.ws = null;
|
|
@@ -1658,9 +1902,7 @@ export class OpenClawClient {
|
|
|
1658
1902
|
this.sessionMessageCallbacks.set(`agent:main:${sessionKey}`, onMessage);
|
|
1659
1903
|
this.subscribedKeys.add(sessionKey);
|
|
1660
1904
|
try {
|
|
1661
|
-
|
|
1662
|
-
await this.rpc("sessions.messages.subscribe", { key: sessionKey }).catch(() => { });
|
|
1663
|
-
await this.rpc("sessions.messages.subscribe", { key: `agent:main:${sessionKey}` }).catch(() => { });
|
|
1905
|
+
await this.rpc("sessions.messages.subscribe", { key: this.canonicalSessionKey(sessionKey) });
|
|
1664
1906
|
}
|
|
1665
1907
|
catch (err) {
|
|
1666
1908
|
console.warn(`[OpenClaw] Failed to subscribe ${sessionKey}:`, err.message);
|
|
@@ -1685,7 +1927,7 @@ export class OpenClawClient {
|
|
|
1685
1927
|
this.setVerboseTranscriptDelivery(sessionKey, false);
|
|
1686
1928
|
this.subscribedKeys.delete(sessionKey);
|
|
1687
1929
|
try {
|
|
1688
|
-
await this.rpc("sessions.messages.unsubscribe", { key: sessionKey });
|
|
1930
|
+
await this.rpc("sessions.messages.unsubscribe", { key: this.canonicalSessionKey(sessionKey) });
|
|
1689
1931
|
}
|
|
1690
1932
|
catch {
|
|
1691
1933
|
// ignore
|