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.
@@ -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
- if (this.recoveryStore?.isStoppedObservation(this.canonicalSessionKey(sessionKey), runId))
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 || "chat 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 ? `agent:main:${targetSessionKey.replace(/^agent:[^:]+:/, "")}` : "";
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 === "number" && ended < 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 Error(`Agent error: ${typeof snapshot.error === "string" && snapshot.error.trim()
1361
- ? snapshot.error : `Gateway 已确认运行以 ${snapshot.status} 结束`}`));
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(new Error(`Agent error: ${detail}`));
1789
- }, error => { void handleTimeoutResult(error, () => idleGeneration === decisionGeneration); });
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) || typeof snapshot.startedAt === 'number' && snapshot.endedAt < snapshot.startedAt
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.formatTerminalRunError(snapshot);
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 /^agent:[^:]+:/.test(sessionKey) ? sessionKey : `agent:main:${sessionKey}`;
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.startsWith("agent:") ? sessionKey : `agent:main:${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 = [payload?.stopReason, payload?.message?.stopReason].find(reason => reason === "error" || reason === "aborted")
2676
- || payload?.stopReason || payload?.message?.stopReason;
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
- ...(typeof stopReason === "string" ? { stopReason } : {}), detail,
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 = sessionKey.startsWith("agent:main:") ? sessionKey.slice("agent:main:".length) : sessionKey;
2884
- const fullKey = `agent:main:${shortKey}`;
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(`agent:main:${sessionKey}`, onMessage);
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, `agent:main:${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(`agent:main:${sessionKey}`);
3681
+ this.sessionMessageCallbacks.delete(withAgentPrefix(sessionKey));
3571
3682
  this.toolEventCallbacks.delete(sessionKey);
3572
- this.toolEventCallbacks.delete(`agent:main:${sessionKey}`);
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(`agent:main:${sessionKey}`, callback);
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): LiveStatusController | undefined;
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
  }