openclaw-lark-multi-agent 1.8.19 → 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 +47 -7
- package/dist/feishu-bot.js +423 -163
- package/dist/go-controller.d.ts +2 -0
- package/dist/go-controller.js +24 -8
- package/dist/input-status-display.d.ts +45 -0
- package/dist/input-status-display.js +67 -0
- package/dist/live-status.d.ts +22 -3
- package/dist/live-status.js +93 -25
- package/dist/message-store.d.ts +68 -2
- package/dist/message-store.js +249 -13
- package/dist/openclaw-client.d.ts +0 -6
- package/dist/openclaw-client.js +178 -67
- package/dist/queued-presentation.d.ts +8 -1
- package/dist/queued-presentation.js +109 -4
- package/dist/terminal-errors.d.ts +12 -0
- package/dist/terminal-errors.js +23 -3
- package/docs/releases/1.8.20.md +30 -0
- package/package.json +4 -3
package/dist/openclaw-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { safeErrorSummary, rawErrorExcerpt } from "./provider-errors.js";
|
|
3
|
-
import { inspectError, genericFailure, announceCandidate } from "./terminal-errors.js";
|
|
3
|
+
import { RunTerminalError, closedStopReason, inspectError, genericFailure, announceCandidate } from "./terminal-errors.js";
|
|
4
4
|
import { SessionWaitPaused, normalizeSessionRuntimeStatus, isWaitTimeout, FOREGROUND_WAIT_MS, IDLE_GUARD_MS } from "./session-status.js";
|
|
5
5
|
import { createHash, randomUUID } from "crypto";
|
|
6
6
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
@@ -74,10 +74,21 @@ const EVENT_PROGRESS_BUCKET_BYTES = 256 * 1024;
|
|
|
74
74
|
const EVENT_PROGRESS_TOTAL_BYTES = 4 * 1024 * 1024;
|
|
75
75
|
const EVENT_PROGRESS_FIELD_CHARS = 2048;
|
|
76
76
|
const MAX_INLINE_CONTEXT_BYTES = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_MAX_INLINE_CONTEXT_BYTES || 128 * 1024);
|
|
77
|
+
const reasonPriority = (r) => r === 'superseded' || r === 'restart' ? 4 : r === 'timeout' ? 3 : r === 'error' || r === 'aborted' ? 2 : closedStopReason(r) ? 1 : 0;
|
|
78
|
+
const terminalReason = (...values) => values.map(closedStopReason).sort((a, b) => reasonPriority(b) - reasonPriority(a)).find(Boolean);
|
|
77
79
|
/**
|
|
78
80
|
* OpenClaw Gateway WebSocket client.
|
|
79
81
|
* Full agent pipeline — tools, memory, skills, context management by OpenClaw.
|
|
80
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:[^:]+:/, '');
|
|
81
92
|
export class OpenClawClient {
|
|
82
93
|
goEvidenceObservers = new Set();
|
|
83
94
|
/** Raw full event observer. Observation is NOT a Feishu delivery receipt. */
|
|
@@ -151,7 +162,11 @@ export class OpenClawClient {
|
|
|
151
162
|
isExplicitlyStoppedRun(sessionKey, runId) {
|
|
152
163
|
if (!runId)
|
|
153
164
|
return false;
|
|
154
|
-
|
|
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))
|
|
155
170
|
return true;
|
|
156
171
|
const key = JSON.stringify([this.canonicalSessionKey(sessionKey), runId]);
|
|
157
172
|
const at = this.explicitlyStoppedRuns.get(key);
|
|
@@ -225,7 +240,7 @@ export class OpenClawClient {
|
|
|
225
240
|
if (['error', 'timeout'].includes(snapshot.status)) {
|
|
226
241
|
// endedAt + exact run is terminal evidence; bare query timeouts never get here.
|
|
227
242
|
await callback?.(String(snapshot.error || snapshot.status), { sourceType: 'run_error', sessionKey: item.sessionKey, runId: item.runId,
|
|
228
|
-
error: { sessionKey: item.sessionKey, runId: item.runId, source: 'chat', state: 'error', detail: String(snapshot.error || snapshot.status) } });
|
|
243
|
+
error: { sessionKey: item.sessionKey, sessionId: item.sessionId, runId: item.runId, source: 'chat', state: 'error', stopReason: closedStopReason(snapshot.stopReason) || (snapshot.status === 'timeout' ? 'timeout' : undefined), detail: String(snapshot.error || snapshot.status) } });
|
|
229
244
|
this.recoveryStore.settleExactRunReactions(item.sessionKey, item.runId);
|
|
230
245
|
this.recoveryStore.finishRunObservation(item.sessionKey, item.runId);
|
|
231
246
|
await this.sessionSettlementCallbacks.get(this.canonicalSessionKey(item.sessionKey))?.({ sessionKey: item.sessionKey, runId: item.runId, sessionId: item.sessionId, final: true });
|
|
@@ -297,13 +312,13 @@ export class OpenClawClient {
|
|
|
297
312
|
* payload. Keep only the fields collectReply actually consumes here. */
|
|
298
313
|
projectBufferedEvent(event) {
|
|
299
314
|
const data = event.data || {}, clip = (value) => typeof value === 'string' ? value.slice(0, EVENT_PROGRESS_FIELD_CHARS) : value;
|
|
300
|
-
const out = { runId: event.runId, sessionKey: event.sessionKey, stream: event.stream, seq: event.seq, data: {} };
|
|
315
|
+
const out = { runId: event.runId, sessionKey: event.sessionKey, sessionId: event.sessionId, stream: event.stream, seq: event.seq, data: {} };
|
|
301
316
|
if (event.stream === 'chatFinal') {
|
|
302
317
|
out.data = { text: data.gatewayYielded === true ? '' : typeof data.text === 'string' ? data.text : '', gatewayYielded: data.gatewayYielded === true };
|
|
303
318
|
return out; // Entire authority, not a preview; no duplicated message body.
|
|
304
319
|
}
|
|
305
320
|
if (event.stream === 'chatError') {
|
|
306
|
-
out.data = { error: data.error };
|
|
321
|
+
out.data = { error: data.error, state: data.state, stopReason: closedStopReason(data.stopReason) };
|
|
307
322
|
return out;
|
|
308
323
|
}
|
|
309
324
|
const keep = event.stream === 'lifecycle'
|
|
@@ -572,7 +587,7 @@ export class OpenClawClient {
|
|
|
572
587
|
data: { text: textParts.join("\n"), gatewayYielded: frame.payload?.yielded === true },
|
|
573
588
|
});
|
|
574
589
|
}
|
|
575
|
-
else if (state === "error") {
|
|
590
|
+
else if (state === "error" || state === "aborted") {
|
|
576
591
|
// A chat-level error (e.g. "Request timed out before a response")
|
|
577
592
|
// must NOT be silently dropped — otherwise mid-run delta text gets
|
|
578
593
|
// surfaced as if it were a normal reply and the run neither errors
|
|
@@ -580,7 +595,8 @@ export class OpenClawClient {
|
|
|
580
595
|
this.bufferEvent(sk, {
|
|
581
596
|
...frame.payload,
|
|
582
597
|
stream: "chatError",
|
|
583
|
-
data: { error: frame.payload?.errorMessage || frame.payload?.error ||
|
|
598
|
+
data: { error: frame.payload?.errorMessage || frame.payload?.error || (state === 'aborted' ? 'aborted' : 'chat error'),
|
|
599
|
+
state, stopReason: terminalReason(frame.payload?.stopReason, frame.payload?.message?.stopReason) },
|
|
584
600
|
});
|
|
585
601
|
}
|
|
586
602
|
}
|
|
@@ -1166,7 +1182,7 @@ export class OpenClawClient {
|
|
|
1166
1182
|
// structured content arrays rather than a plain string. Extract only visible
|
|
1167
1183
|
// text parts and ignore thinking/tool blocks so the bridge can deliver final
|
|
1168
1184
|
// cron results via the bot.
|
|
1169
|
-
if (msg?.role === "assistant" && ["error", "aborted"].includes(msg?.stopReason)) {
|
|
1185
|
+
if (msg?.role === "assistant" && ["error", "aborted", "timeout", "restart", "superseded"].includes(msg?.stopReason)) {
|
|
1170
1186
|
if (this.mutedProactiveSessions.has(rawKey) || this.mutedProactiveSessions.has(shortKey))
|
|
1171
1187
|
return false;
|
|
1172
1188
|
if (runId && this.ownedDeliveryRuns.has(runId))
|
|
@@ -1174,7 +1190,7 @@ export class OpenClawClient {
|
|
|
1174
1190
|
return this.emitProactiveForSession(rawKey, String(msg.errorMessage || "Agent run failed"), {
|
|
1175
1191
|
sourceType: "run_error", runId,
|
|
1176
1192
|
error: { sessionKey: this.canonicalSessionKey(rawKey), runId: runId || "", source: "session.message",
|
|
1177
|
-
state: "final", stopReason: msg.stopReason, detail: String(msg.errorMessage || ""),
|
|
1193
|
+
state: "final", sessionId, stopReason: closedStopReason(msg.stopReason), detail: String(msg.errorMessage || ""),
|
|
1178
1194
|
...(this.isExplicitlyStoppedRun(rawKey, runId) ? { userStop: true } : {}) },
|
|
1179
1195
|
});
|
|
1180
1196
|
}
|
|
@@ -1252,7 +1268,7 @@ export class OpenClawClient {
|
|
|
1252
1268
|
// tripping the disconnect path.
|
|
1253
1269
|
const startedConnected = this.connected;
|
|
1254
1270
|
let chatFinalText = "";
|
|
1255
|
-
let sessionKey = targetSessionKey ?
|
|
1271
|
+
let sessionKey = targetSessionKey ? withAgentPrefix(targetSessionKey) : "";
|
|
1256
1272
|
let shortSessionKey = targetSessionKey ? targetSessionKey.replace(/^agent:[^:]+:/, "") : "";
|
|
1257
1273
|
let chatFinalTimer = null;
|
|
1258
1274
|
let lifecycleEndTimer = null;
|
|
@@ -1264,6 +1280,7 @@ export class OpenClawClient {
|
|
|
1264
1280
|
// root's recorded terminal state (all tools settled, no error, no async
|
|
1265
1281
|
// pending) before anything resolves.
|
|
1266
1282
|
let terminatingReconcileTimer = null;
|
|
1283
|
+
let terminalHint;
|
|
1267
1284
|
let pendingRuntimeFailureText = "";
|
|
1268
1285
|
let lastActivitySummary = "";
|
|
1269
1286
|
let lastActivityAt = 0;
|
|
@@ -1331,14 +1348,14 @@ export class OpenClawClient {
|
|
|
1331
1348
|
collectEvents();
|
|
1332
1349
|
if (!idleStillCurrent() || idleGeneration !== decisionGeneration)
|
|
1333
1350
|
return;
|
|
1334
|
-
if (observationClosed || snapshot?.runId !== runId || snapshot?.pendingError === true
|
|
1351
|
+
if (observationClosed || snapshot?.runId !== runId || options?.expectedSessionId && snapshot?.sessionId && snapshot.sessionId !== options.expectedSessionId || snapshot?.pendingError === true
|
|
1335
1352
|
|| snapshot?.yielded === true || snapshot?.timeoutPhase === "queue" || snapshot?.timeoutPhase === "gateway_draining")
|
|
1336
1353
|
return;
|
|
1337
1354
|
// A bare wait timeout is not an execution timeout. Require a recorded
|
|
1338
1355
|
// terminal time; never infer failure from stopReason or replay flags.
|
|
1339
1356
|
const ended = snapshot.endedAt;
|
|
1340
1357
|
if (typeof ended !== "number" || !Number.isFinite(ended) || ended <= 0
|
|
1341
|
-
|| (typeof snapshot.startedAt
|
|
1358
|
+
|| (snapshot.startedAt !== undefined && (typeof snapshot.startedAt !== 'number' || !Number.isFinite(snapshot.startedAt) || snapshot.startedAt < 0 || ended < snapshot.startedAt)))
|
|
1342
1359
|
return;
|
|
1343
1360
|
if (snapshot.status === "ok" && snapshot.yielded !== true) {
|
|
1344
1361
|
const terminal = snapshot.terminalReply;
|
|
@@ -1357,8 +1374,9 @@ export class OpenClawClient {
|
|
|
1357
1374
|
else if (snapshot.status === "error" || snapshot.status === "timeout") {
|
|
1358
1375
|
if (observationClosed)
|
|
1359
1376
|
return;
|
|
1360
|
-
reject(new
|
|
1361
|
-
|
|
1377
|
+
reject(new RunTerminalError({ sessionKey, sessionId: options?.expectedSessionId, runId, source: 'chat', state: 'error',
|
|
1378
|
+
stopReason: closedStopReason(snapshot.stopReason) || (snapshot.status === 'timeout' ? 'timeout' : undefined),
|
|
1379
|
+
detail: typeof snapshot.error === 'string' ? snapshot.error : undefined }));
|
|
1362
1380
|
}
|
|
1363
1381
|
}
|
|
1364
1382
|
catch { /* Unavailable evidence means keep waiting, never abort/replay. */ }
|
|
@@ -1542,6 +1560,106 @@ export class OpenClawClient {
|
|
|
1542
1560
|
clearTimeout(terminatingReconcileTimer);
|
|
1543
1561
|
resolve(finalText);
|
|
1544
1562
|
};
|
|
1563
|
+
// One bounded public exact-run reconciliation for generic aborts. A
|
|
1564
|
+
// closed wire cause wins over generic later prose, never another run.
|
|
1565
|
+
const deferTerminal = (ev, detail) => {
|
|
1566
|
+
const reason = closedStopReason(ev.data?.stopReason);
|
|
1567
|
+
const p = { sessionKey, sessionId: options?.expectedSessionId || ev.sessionId, runId,
|
|
1568
|
+
source: 'chat', state: ev.data?.state === 'aborted' || reason ? 'aborted' : 'error', stopReason: reason, detail };
|
|
1569
|
+
if (!terminalHint || reasonPriority(reason) > reasonPriority(terminalHint.stopReason))
|
|
1570
|
+
terminalHint = p;
|
|
1571
|
+
if (genericChatErrorTimer)
|
|
1572
|
+
return;
|
|
1573
|
+
const abortGeneration = idleGeneration;
|
|
1574
|
+
genericChatErrorTimer = setTimeout(() => {
|
|
1575
|
+
genericChatErrorTimer = null;
|
|
1576
|
+
void (async () => {
|
|
1577
|
+
collectEvents();
|
|
1578
|
+
if (observationClosed)
|
|
1579
|
+
return;
|
|
1580
|
+
if (!terminalHint?.stopReason && idleGeneration !== abortGeneration) {
|
|
1581
|
+
terminalHint = undefined;
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (chatFinalText.trim()) {
|
|
1585
|
+
finish(chatFinalText);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
let candidate = terminalHint;
|
|
1589
|
+
if (!candidate.stopReason) {
|
|
1590
|
+
try {
|
|
1591
|
+
const snapshot = await this.rpc('agent.wait', { runId, timeoutMs: 1 }, 2000);
|
|
1592
|
+
collectEvents();
|
|
1593
|
+
if (observationClosed)
|
|
1594
|
+
return;
|
|
1595
|
+
if (options?.expectedSessionId && snapshot?.sessionId && snapshot.sessionId !== options.expectedSessionId)
|
|
1596
|
+
return;
|
|
1597
|
+
if (chatFinalText.trim()) {
|
|
1598
|
+
finish(chatFinalText);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
// The query sampled the provisional abort's old liveness cut.
|
|
1602
|
+
// Drain first, then fence even a recorded terminal snapshot if
|
|
1603
|
+
// verified same-run progress advanced while this read awaited.
|
|
1604
|
+
if (!terminalHint?.stopReason && idleGeneration !== abortGeneration) {
|
|
1605
|
+
terminalHint = undefined;
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (terminalHint?.stopReason)
|
|
1609
|
+
candidate = terminalHint;
|
|
1610
|
+
else if (snapshot?.runId === runId && !snapshot.pendingError && !snapshot.yielded &&
|
|
1611
|
+
!['queue', 'gateway_draining'].includes(snapshot.timeoutPhase) && Number.isFinite(snapshot.endedAt) && snapshot.endedAt > 0 &&
|
|
1612
|
+
(snapshot.startedAt === undefined || typeof snapshot.startedAt === 'number' && Number.isFinite(snapshot.startedAt) && snapshot.startedAt >= 0 && snapshot.endedAt >= snapshot.startedAt)) {
|
|
1613
|
+
if (snapshot.status === 'ok' && snapshot.terminalReply?.disposition === 'visible' && typeof snapshot.terminalReply.text === 'string') {
|
|
1614
|
+
finish(snapshot.terminalReply.text);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (snapshot.status === 'ok' && ['silent', 'empty'].includes(snapshot.terminalReply?.disposition)) {
|
|
1618
|
+
finish('NO_REPLY');
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (!['error', 'timeout'].includes(snapshot.status))
|
|
1622
|
+
return;
|
|
1623
|
+
candidate = { ...candidate, state: 'error', stopReason: closedStopReason(snapshot.stopReason) || (snapshot.status === 'timeout' ? 'timeout' : undefined),
|
|
1624
|
+
detail: typeof snapshot.error === 'string' ? snapshot.error : candidate.detail };
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
if (snapshot?.pendingError || snapshot?.yielded || ['queue', 'gateway_draining'].includes(snapshot?.timeoutPhase)) {
|
|
1628
|
+
terminalHint = undefined;
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
// A wait-only timeout adds NO terminal cause. The actual
|
|
1632
|
+
// abort/error envelope can still be reported as unknown.
|
|
1633
|
+
candidate = { ...candidate, state: 'error', stopReason: undefined };
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
catch { /* No cause proof: preserve the generic, unclassified diagnostic. */ }
|
|
1637
|
+
}
|
|
1638
|
+
// A rejected lookup crosses the same await boundary. It cannot
|
|
1639
|
+
// revive a stale generic hint either; new wire final/closed metadata
|
|
1640
|
+
// and explicit-stop receipts still outrank that provisional cut.
|
|
1641
|
+
collectEvents();
|
|
1642
|
+
if (observationClosed)
|
|
1643
|
+
return;
|
|
1644
|
+
if (chatFinalText.trim()) {
|
|
1645
|
+
finish(chatFinalText);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (!terminalHint?.stopReason && idleGeneration !== abortGeneration) {
|
|
1649
|
+
terminalHint = undefined;
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
if (terminalHint?.stopReason && reasonPriority(terminalHint.stopReason) > reasonPriority(candidate.stopReason))
|
|
1653
|
+
candidate = terminalHint;
|
|
1654
|
+
if (this.isExplicitlyStoppedRun(sessionKey, runId)) {
|
|
1655
|
+
finish('NO_REPLY');
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
reject(new RunTerminalError(candidate));
|
|
1659
|
+
})().catch(error => { if (!observationClosed)
|
|
1660
|
+
reject(error); });
|
|
1661
|
+
}, 350);
|
|
1662
|
+
};
|
|
1545
1663
|
const collectEvents = () => {
|
|
1546
1664
|
if (observationClosed)
|
|
1547
1665
|
return;
|
|
@@ -1577,7 +1695,7 @@ export class OpenClawClient {
|
|
|
1577
1695
|
// Non-destructive per-observer cursor: ACK and a replacement final
|
|
1578
1696
|
// may be delivered synchronously before the awaiting sender resumes.
|
|
1579
1697
|
// Leave every foreign/unidentified frame available to its own owner.
|
|
1580
|
-
if (!matchesRun || sessionKey && !eventSessionMatches)
|
|
1698
|
+
if (!matchesRun || sessionKey && !eventSessionMatches || options?.expectedSessionId && ev.sessionId && ev.sessionId !== options.expectedSessionId)
|
|
1581
1699
|
continue;
|
|
1582
1700
|
seenEvents.add(ev);
|
|
1583
1701
|
if (!sessionKey && ev.sessionKey) {
|
|
@@ -1720,6 +1838,10 @@ export class OpenClawClient {
|
|
|
1720
1838
|
}
|
|
1721
1839
|
if (ev.stream === "lifecycle" && ev.data?.phase === "error") {
|
|
1722
1840
|
const errText = String(ev.data?.error || "unknown");
|
|
1841
|
+
if (closedStopReason(ev.data?.stopReason) && ev.data.stopReason !== 'error' || /^(?:agent run )?aborted$/i.test(errText.trim()) || terminalHint) {
|
|
1842
|
+
deferTerminal(ev, errText);
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1723
1845
|
// Some agent errors are recoverable: OpenClaw emits a lifecycle
|
|
1724
1846
|
// error (e.g. "Context overflow: prompt too large") and then
|
|
1725
1847
|
// auto-compacts and KEEPS RUNNING the same run, eventually emitting
|
|
@@ -1761,6 +1883,10 @@ export class OpenClawClient {
|
|
|
1761
1883
|
// surfaces the failure / lets auto-retry kick in — instead of
|
|
1762
1884
|
// surfacing whatever mid-run delta text happened to be collected.
|
|
1763
1885
|
const errText = String(ev.data?.error || "chat error");
|
|
1886
|
+
if (ev.data?.state === 'aborted' || closedStopReason(ev.data?.stopReason) && ev.data.stopReason !== 'error' || /^(?:agent run )?aborted$/i.test(errText.trim()) || terminalHint) {
|
|
1887
|
+
deferTerminal(ev, errText);
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1764
1890
|
if (this.isGenericChatError(errText)) {
|
|
1765
1891
|
// Gateway can emit a terminal chat error without errorMessage,
|
|
1766
1892
|
// then persist a richer terminal snapshot a moment later. Do
|
|
@@ -1770,8 +1896,12 @@ export class OpenClawClient {
|
|
|
1770
1896
|
if (!genericChatErrorTimer) {
|
|
1771
1897
|
genericChatErrorTimer = setTimeout(() => {
|
|
1772
1898
|
genericChatErrorTimer = null;
|
|
1899
|
+
if (terminalHint) {
|
|
1900
|
+
deferTerminal({ data: terminalHint }, terminalHint.detail || 'aborted');
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1773
1903
|
const decisionGeneration = idleGeneration;
|
|
1774
|
-
void this.resolveTerminalChatError(evRunId || runId, targetSessionKey || sessionKey).then((detail) => {
|
|
1904
|
+
void this.resolveTerminalChatError(evRunId || runId, targetSessionKey || sessionKey, options?.expectedSessionId).then((detail) => {
|
|
1775
1905
|
collectEvents();
|
|
1776
1906
|
if (observationClosed || idleGeneration !== decisionGeneration)
|
|
1777
1907
|
return;
|
|
@@ -1779,14 +1909,24 @@ export class OpenClawClient {
|
|
|
1779
1909
|
finish(chatFinalText);
|
|
1780
1910
|
return;
|
|
1781
1911
|
}
|
|
1912
|
+
if (terminalHint) {
|
|
1913
|
+
deferTerminal({ data: terminalHint }, terminalHint.detail || 'aborted');
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1782
1916
|
clearTimeout(idleTimer);
|
|
1783
1917
|
clearInterval(poller);
|
|
1784
1918
|
if (chatFinalTimer)
|
|
1785
1919
|
clearTimeout(chatFinalTimer);
|
|
1786
1920
|
if (lifecycleEndTimer)
|
|
1787
1921
|
clearTimeout(lifecycleEndTimer);
|
|
1788
|
-
reject(
|
|
1789
|
-
}, error => {
|
|
1922
|
+
reject(detail);
|
|
1923
|
+
}, error => {
|
|
1924
|
+
if (terminalHint) {
|
|
1925
|
+
deferTerminal({ data: terminalHint }, terminalHint.detail || 'aborted');
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
void handleTimeoutResult(error, () => idleGeneration === decisionGeneration);
|
|
1929
|
+
});
|
|
1790
1930
|
}, 350);
|
|
1791
1931
|
}
|
|
1792
1932
|
continue;
|
|
@@ -1922,15 +2062,17 @@ export class OpenClawClient {
|
|
|
1922
2062
|
const t = (errText || "").trim().toLowerCase();
|
|
1923
2063
|
return t === "chat error" || t === "agent error: chat error";
|
|
1924
2064
|
}
|
|
1925
|
-
async resolveTerminalChatError(runId, sessionKey) {
|
|
2065
|
+
async resolveTerminalChatError(runId, sessionKey, expectedSessionId) {
|
|
1926
2066
|
try {
|
|
1927
2067
|
const snapshot = await this.rpc("agent.wait", { runId, timeoutMs: 1500 }, 2500);
|
|
1928
|
-
if (snapshot?.runId !== runId || !Number.isFinite(snapshot.endedAt) || snapshot.endedAt <= 0 || snapshot.yielded || snapshot.pendingError
|
|
1929
|
-
|| ['queue', 'gateway_draining'].includes(snapshot.timeoutPhase) ||
|
|
2068
|
+
if (snapshot?.runId !== runId || expectedSessionId && snapshot?.sessionId && snapshot.sessionId !== expectedSessionId || !Number.isFinite(snapshot.endedAt) || snapshot.endedAt <= 0 || snapshot.yielded || snapshot.pendingError
|
|
2069
|
+
|| ['queue', 'gateway_draining'].includes(snapshot.timeoutPhase) || snapshot.startedAt !== undefined &&
|
|
2070
|
+
(typeof snapshot.startedAt !== 'number' || !Number.isFinite(snapshot.startedAt) || snapshot.startedAt < 0 || snapshot.endedAt < snapshot.startedAt)
|
|
1930
2071
|
|| !['error', 'timeout'].includes(snapshot.status)) {
|
|
1931
2072
|
throw new SessionWaitPaused(await this.getSessionRuntimeStatus(sessionKey || ''), true);
|
|
1932
2073
|
}
|
|
1933
|
-
return this.
|
|
2074
|
+
return new RunTerminalError({ sessionKey: this.canonicalSessionKey(sessionKey || ''), sessionId: expectedSessionId || snapshot.sessionId, runId, source: 'chat', state: 'error',
|
|
2075
|
+
stopReason: closedStopReason(snapshot.stopReason) || (snapshot.status === 'timeout' ? 'timeout' : undefined), detail: typeof snapshot.error === 'string' ? snapshot.error : undefined });
|
|
1934
2076
|
}
|
|
1935
2077
|
catch (err) {
|
|
1936
2078
|
if (err instanceof SessionWaitPaused)
|
|
@@ -1939,36 +2081,6 @@ export class OpenClawClient {
|
|
|
1939
2081
|
throw new SessionWaitPaused(normalizeSessionRuntimeStatus(undefined), true);
|
|
1940
2082
|
}
|
|
1941
2083
|
}
|
|
1942
|
-
formatTerminalRunError(snapshot) {
|
|
1943
|
-
const rawError = typeof snapshot?.error === "string" ? snapshot.error.trim() : "";
|
|
1944
|
-
const timeoutPhase = typeof snapshot?.timeoutPhase === "string" ? snapshot.timeoutPhase.trim() : "";
|
|
1945
|
-
const stopReason = typeof snapshot?.stopReason === "string" ? snapshot.stopReason.trim() : "";
|
|
1946
|
-
const status = typeof snapshot?.status === "string" ? snapshot.status.trim() : "";
|
|
1947
|
-
const timedOut = status === "timeout" || Boolean(timeoutPhase) || /timed?\s*out|timeout/i.test(rawError);
|
|
1948
|
-
if (timedOut) {
|
|
1949
|
-
const startedAt = Number(snapshot?.startedAt);
|
|
1950
|
-
const endedAt = Number(snapshot?.endedAt);
|
|
1951
|
-
const elapsedSeconds = Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt > startedAt
|
|
1952
|
-
? Math.round((endedAt - startedAt) / 1000)
|
|
1953
|
-
: 0;
|
|
1954
|
-
const elapsed = elapsedSeconds > 0 ? `(运行约 ${this.formatDurationSeconds(elapsedSeconds)})` : "";
|
|
1955
|
-
const phase = timeoutPhase ? `,阶段:${timeoutPhase}` : "";
|
|
1956
|
-
return `等待 Agent 运行结果超时${elapsed}${phase};底层运行可能仍在继续${rawError ? `;${rawError}` : ""}`;
|
|
1957
|
-
}
|
|
1958
|
-
if (rawError)
|
|
1959
|
-
return rawError;
|
|
1960
|
-
if (stopReason === "stop" || stopReason === "rpc")
|
|
1961
|
-
return `Agent 被停止(${stopReason})`;
|
|
1962
|
-
const meta = [status && `状态=${status}`, stopReason && `原因=${stopReason}`, timeoutPhase && `阶段=${timeoutPhase}`].filter(Boolean).join(",");
|
|
1963
|
-
return meta ? `OpenClaw 运行失败:${meta}` : "OpenClaw 运行失败,但未返回具体原因";
|
|
1964
|
-
}
|
|
1965
|
-
formatDurationSeconds(seconds) {
|
|
1966
|
-
if (seconds % 3600 === 0)
|
|
1967
|
-
return `${seconds / 3600} 小时`;
|
|
1968
|
-
if (seconds % 60 === 0)
|
|
1969
|
-
return `${seconds / 60} 分钟`;
|
|
1970
|
-
return `${seconds} 秒`;
|
|
1971
|
-
}
|
|
1972
2084
|
isRecoverableAgentError(errText) {
|
|
1973
2085
|
const t = (errText || "").toLowerCase();
|
|
1974
2086
|
return t.includes("context overflow")
|
|
@@ -2128,7 +2240,7 @@ export class OpenClawClient {
|
|
|
2128
2240
|
return sessionKey.replace(/^agent:[^:]+:/, "");
|
|
2129
2241
|
}
|
|
2130
2242
|
canonicalSessionKey(sessionKey) {
|
|
2131
|
-
return
|
|
2243
|
+
return withAgentPrefix(sessionKey);
|
|
2132
2244
|
}
|
|
2133
2245
|
registerPendingSteer(sessionKey, text, displayText) {
|
|
2134
2246
|
const key = this.shortKey(sessionKey);
|
|
@@ -2434,7 +2546,7 @@ export class OpenClawClient {
|
|
|
2434
2546
|
const message = firstErr.message || String(firstErr);
|
|
2435
2547
|
if (!/session (?:not found|unknown)|unknown session|invalid session key/i.test(message))
|
|
2436
2548
|
throw firstErr;
|
|
2437
|
-
const fullKey = sessionKey
|
|
2549
|
+
const fullKey = withAgentPrefix(sessionKey);
|
|
2438
2550
|
held?.guard();
|
|
2439
2551
|
await this.patchSession({ key: fullKey, model: expectedModel, ...(held ? { expectedSessionId: held.sessionId } : {}) });
|
|
2440
2552
|
}
|
|
@@ -2644,7 +2756,7 @@ export class OpenClawClient {
|
|
|
2644
2756
|
for (const stopped of result.runIds)
|
|
2645
2757
|
if (typeof stopped === "string" && stopped) {
|
|
2646
2758
|
this.explicitlyStoppedRuns.set(JSON.stringify([key, stopped]), Date.now());
|
|
2647
|
-
this.recoveryStore?.stopExactRun(key, stopped);
|
|
2759
|
+
this.recoveryStore?.stopExactRun(key, stopped, true);
|
|
2648
2760
|
}
|
|
2649
2761
|
}
|
|
2650
2762
|
return result;
|
|
@@ -2672,17 +2784,16 @@ export class OpenClawClient {
|
|
|
2672
2784
|
}
|
|
2673
2785
|
if (runId && this.isExplicitlyStoppedRun(sessionKey, runId) && state === 'final')
|
|
2674
2786
|
return;
|
|
2675
|
-
const stopReason =
|
|
2676
|
-
|
|
2677
|
-
const isError = state === "error" || state === "aborted" || stopReason === "error" || stopReason === "aborted";
|
|
2787
|
+
const stopReason = terminalReason(payload?.stopReason, payload?.message?.stopReason);
|
|
2788
|
+
const isError = state === "error" || state === "aborted" || ['error', 'aborted', 'timeout', 'restart', 'superseded'].includes(stopReason || '');
|
|
2678
2789
|
if (isError) {
|
|
2679
2790
|
const detail = String(payload?.errorMessage || payload?.message?.errorMessage || payload?.error || payload?.reason || "").trim();
|
|
2680
2791
|
const aborted = state === "aborted" || stopReason === "aborted";
|
|
2681
2792
|
const text = `⚠️ Agent run ${aborted ? "was aborted" : "failed"}${detail ? `: ${detail}` : ""}`;
|
|
2682
2793
|
this.emitProactiveForSession(sessionKey, text, {
|
|
2683
2794
|
sourceType: "run_error", runId,
|
|
2684
|
-
error: { sessionKey: this.canonicalSessionKey(sessionKey), runId, source: "chat", state: state || "final",
|
|
2685
|
-
...(
|
|
2795
|
+
error: { sessionKey: this.canonicalSessionKey(sessionKey), runId, source: "chat", state: state || "final", sessionId: typeof payload?.sessionId === 'string' ? payload.sessionId : undefined,
|
|
2796
|
+
...(closedStopReason(stopReason) ? { stopReason: closedStopReason(stopReason) } : {}), detail,
|
|
2686
2797
|
...(this.isExplicitlyStoppedRun(sessionKey, runId) ? { userStop: true } : {}) },
|
|
2687
2798
|
});
|
|
2688
2799
|
this.releaseSuppressedSessionKeysAfter(keys, 0);
|
|
@@ -2880,8 +2991,8 @@ export class OpenClawClient {
|
|
|
2880
2991
|
}
|
|
2881
2992
|
}
|
|
2882
2993
|
muteProactiveDelivery(sessionKey) {
|
|
2883
|
-
const shortKey =
|
|
2884
|
-
const fullKey =
|
|
2994
|
+
const shortKey = withoutAgentPrefix(sessionKey);
|
|
2995
|
+
const fullKey = withAgentPrefix(sessionKey);
|
|
2885
2996
|
const keys = [shortKey, fullKey];
|
|
2886
2997
|
for (const key of keys)
|
|
2887
2998
|
this.addMutedProactiveKey(key);
|
|
@@ -3544,7 +3655,7 @@ export class OpenClawClient {
|
|
|
3544
3655
|
if (onSettled)
|
|
3545
3656
|
this.sessionSettlementCallbacks.set(this.canonicalSessionKey(sessionKey), onSettled);
|
|
3546
3657
|
this.sessionMessageCallbacks.set(sessionKey, onMessage);
|
|
3547
|
-
this.sessionMessageCallbacks.set(
|
|
3658
|
+
this.sessionMessageCallbacks.set(withAgentPrefix(sessionKey), onMessage);
|
|
3548
3659
|
this.subscribedKeys.add(sessionKey);
|
|
3549
3660
|
try {
|
|
3550
3661
|
await this.rpc("sessions.messages.subscribe", { key: this.canonicalSessionKey(sessionKey) });
|
|
@@ -3554,7 +3665,7 @@ export class OpenClawClient {
|
|
|
3554
3665
|
}
|
|
3555
3666
|
}
|
|
3556
3667
|
setVerboseTranscriptDelivery(sessionKey, enabled) {
|
|
3557
|
-
const keys = [sessionKey,
|
|
3668
|
+
const keys = [sessionKey, withAgentPrefix(sessionKey)];
|
|
3558
3669
|
for (const key of keys) {
|
|
3559
3670
|
if (enabled)
|
|
3560
3671
|
this.verboseTranscriptSessions.add(key);
|
|
@@ -3567,9 +3678,9 @@ export class OpenClawClient {
|
|
|
3567
3678
|
async unsubscribeSession(sessionKey) {
|
|
3568
3679
|
this.sessionSettlementCallbacks.delete(this.canonicalSessionKey(sessionKey));
|
|
3569
3680
|
this.sessionMessageCallbacks.delete(sessionKey);
|
|
3570
|
-
this.sessionMessageCallbacks.delete(
|
|
3681
|
+
this.sessionMessageCallbacks.delete(withAgentPrefix(sessionKey));
|
|
3571
3682
|
this.toolEventCallbacks.delete(sessionKey);
|
|
3572
|
-
this.toolEventCallbacks.delete(
|
|
3683
|
+
this.toolEventCallbacks.delete(withAgentPrefix(sessionKey));
|
|
3573
3684
|
this.setVerboseTranscriptDelivery(sessionKey, false);
|
|
3574
3685
|
this.subscribedKeys.delete(sessionKey);
|
|
3575
3686
|
try {
|
|
@@ -3584,6 +3695,6 @@ export class OpenClawClient {
|
|
|
3584
3695
|
*/
|
|
3585
3696
|
onToolEvent(sessionKey, callback) {
|
|
3586
3697
|
this.toolEventCallbacks.set(sessionKey, callback);
|
|
3587
|
-
this.toolEventCallbacks.set(
|
|
3698
|
+
this.toolEventCallbacks.set(withAgentPrefix(sessionKey), callback);
|
|
3588
3699
|
}
|
|
3589
3700
|
}
|
|
@@ -37,8 +37,12 @@ export declare class QueuedPresentations {
|
|
|
37
37
|
private flushRepairs;
|
|
38
38
|
private usable;
|
|
39
39
|
private current;
|
|
40
|
+
private inputContext;
|
|
40
41
|
private hydrate;
|
|
41
|
-
receive(chat: string, rowId: number, messageId: string, native?: boolean, activeRunId?: string
|
|
42
|
+
receive(chat: string, rowId: number, messageId: string, native?: boolean, activeRunId?: string, displayText?: string | {
|
|
43
|
+
source: 'visible';
|
|
44
|
+
text: string;
|
|
45
|
+
}): LiveStatusController | undefined;
|
|
42
46
|
claim(chat: string, ids: number[], ownerId: string, batchId: string, native?: boolean): LiveStatusController | undefined;
|
|
43
47
|
attempted(live: LiveStatusController | undefined, submissionId: string, ownerId: string, batchId: string): void;
|
|
44
48
|
insertionAccepted(chat: string, rowId: number, expectedRunId: string): Promise<void>;
|
|
@@ -53,6 +57,9 @@ export declare class QueuedPresentations {
|
|
|
53
57
|
advance(live: LiveStatusController | undefined, previousRunId: string | undefined, runId: string, ownerId: string, batchId: string): Promise<void>;
|
|
54
58
|
forTarget(chat: string, runId: string, live?: LiveStatusController): LiveStatusController | undefined;
|
|
55
59
|
forRun(chat: string, runId: string): LiveStatusController | undefined;
|
|
60
|
+
/** Recall invalidates copied UI previews, including frozen/terminal cards.
|
|
61
|
+
* It never changes input custody or recreates/retargets an uncertain effect. */
|
|
62
|
+
refreshRecalledInput(chat: string, rowId: number, messageId: string): Promise<void>;
|
|
56
63
|
retire(chat: string): void;
|
|
57
64
|
dispose(): void;
|
|
58
65
|
}
|