openclaw-lark-multi-agent 1.6.0 → 1.7.0

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,4 +1,5 @@
1
1
  import WebSocket from "ws";
2
+ import { safeErrorSummary } from "./provider-errors.js";
2
3
  import { inspectError, genericFailure } from "./terminal-errors.js";
3
4
  import { SessionWaitPaused, normalizeSessionRuntimeStatus, isWaitTimeout, FOREGROUND_WAIT_MS } from "./session-status.js";
4
5
  import { createHash, randomUUID } from "crypto";
@@ -37,6 +38,12 @@ const MAX_INLINE_CONTEXT_BYTES = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_MA
37
38
  * Full agent pipeline — tools, memory, skills, context management by OpenClaw.
38
39
  */
39
40
  export class OpenClawClient {
41
+ goEvidenceObservers = new Set();
42
+ /** Raw full event observer. Observation is NOT a Feishu delivery receipt. */
43
+ observeGoEvidence(callback) {
44
+ this.goEvidenceObservers.add(callback);
45
+ return () => { this.goEvidenceObservers.delete(callback); };
46
+ }
40
47
  config;
41
48
  ws = null;
42
49
  pending = new Map();
@@ -270,6 +277,16 @@ export class OpenClawClient {
270
277
  this.agentEvents.get(sk).push(frame.payload);
271
278
  }
272
279
  }
280
+ if (frame.type === "event" && ["agent", "chat", "session.message"].includes(frame.event)) {
281
+ for (const observer of this.goEvidenceObservers) {
282
+ try {
283
+ observer({ channel: frame.event, payload: frame.payload });
284
+ }
285
+ catch (error) {
286
+ console.warn("[OpenClaw] Go evidence observer failed:", error.message);
287
+ }
288
+ }
289
+ }
273
290
  // Log all events for debugging
274
291
  if (frame.type === "event" && frame.event !== "tick") {
275
292
  console.log(`[OpenClaw] Event: ${frame.event}`, JSON.stringify(frame.payload || {}).substring(0, 200));
@@ -307,7 +324,7 @@ export class OpenClawClient {
307
324
  data: { deltaText: visibleAssistantText, delta: visibleAssistantText, replace: true },
308
325
  });
309
326
  }
310
- this.handleProactiveSessionMessage(rawKey, msg, frame.payload.runId);
327
+ this.handleProactiveSessionMessage(rawKey, msg, frame.payload.runId, frame.payload.sessionId);
311
328
  // Tool calls in assistant messages — skip, using agent item events instead
312
329
  // (session.message toolCall events are batched, not real-time)
313
330
  }
@@ -403,7 +420,7 @@ export class OpenClawClient {
403
420
  if (!handshakeDone)
404
421
  reject(err);
405
422
  else
406
- console.error("[OpenClaw] WS error:", err.message);
423
+ console.error("[OpenClaw] WS error:", safeErrorSummary(err));
407
424
  });
408
425
  this.ws.on("close", () => {
409
426
  this.connected = false;
@@ -651,7 +668,7 @@ export class OpenClawClient {
651
668
  this.flushVerboseAssistantState(rawKey);
652
669
  }
653
670
  }
654
- handleProactiveSessionMessage(rawKey, msg, runId) {
671
+ handleProactiveSessionMessage(rawKey, msg, runId, sessionId) {
655
672
  const shortKey = rawKey.replace(/^agent:[^:]+:/, "");
656
673
  // Proactive assistant text messages. Cron/session-targeted runs often emit
657
674
  // structured content arrays rather than a plain string. Extract only visible
@@ -688,7 +705,7 @@ export class OpenClawClient {
688
705
  const cb = this.sessionMessageCallbacks.get(rawKey) || this.sessionMessageCallbacks.get(shortKey);
689
706
  if (cb) {
690
707
  if (runId)
691
- cb(proactiveText, { runId, sessionKey: this.canonicalSessionKey(rawKey), final: msg?.stopReason === "stop",
708
+ cb(proactiveText, { runId, sessionId, sessionKey: this.canonicalSessionKey(rawKey), final: msg?.stopReason === "stop",
692
709
  ...(!msg?.stopReason && genericFailure(proactiveText) ? { genericErrorCandidate: true } : {}) });
693
710
  else
694
711
  cb(proactiveText);
@@ -706,7 +723,7 @@ export class OpenClawClient {
706
723
  console.log("[OpenClaw] Reconnected successfully");
707
724
  }
708
725
  catch (err) {
709
- console.error("[OpenClaw] Reconnect failed:", err.message);
726
+ console.error("[OpenClaw] Reconnect failed:", safeErrorSummary(err));
710
727
  if (this.shouldReconnect)
711
728
  this.scheduleReconnect();
712
729
  }
@@ -1067,6 +1084,10 @@ export class OpenClawClient {
1067
1084
  const evRunId = typeof ev.runId === "string" ? ev.runId : "";
1068
1085
  const eventSessionMatches = Boolean(sessionKey && (ev.sessionKey === sessionKey || ev.sessionKey === targetSessionKey || ev.sessionKey === shortSessionKey));
1069
1086
  const matchesRun = evRunId ? activeRunIds.has(evRunId) : false;
1087
+ if (options?.exactRunIdentity && !matchesRun) {
1088
+ i++;
1089
+ continue;
1090
+ }
1070
1091
  if (!sessionKey && ev.sessionKey) {
1071
1092
  shortSessionKey = String(ev.sessionKey).replace(/^agent:[^:]+:/, "");
1072
1093
  sessionKey = `agent:main:${shortSessionKey}`;
@@ -1114,7 +1135,7 @@ export class OpenClawClient {
1114
1135
  continue;
1115
1136
  }
1116
1137
  let matchesSession = false;
1117
- if (eventSessionMatches && anchorSeen) {
1138
+ if (!options?.exactRunIdentity && eventSessionMatches && anchorSeen) {
1118
1139
  if (!expectedUserText) {
1119
1140
  // Backward-compatible mode for tests/internal callers that do not
1120
1141
  // provide an anchor: allow session-key matching as before.
@@ -1233,7 +1254,7 @@ export class OpenClawClient {
1233
1254
  // Never abort a yielded original run, nor an empty final.
1234
1255
  if (!foregroundYielded && latestFinalText)
1235
1256
  this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
1236
- console.warn(`[OpenClaw] abort after chatFinal fallback failed:`, err.message);
1257
+ console.warn(`[OpenClaw] abort after chatFinal fallback failed:`, safeErrorSummary(err));
1237
1258
  });
1238
1259
  if (latestFinalText) {
1239
1260
  finish(latestFinalText);
@@ -1330,8 +1351,8 @@ export class OpenClawClient {
1330
1351
  // real final wins; if the run truly stalls, the idle timeout falls
1331
1352
  // back to this text.
1332
1353
  if (this.isRecoverableAgentError(errText)) {
1333
- pendingRuntimeFailureText = `⚠️ Agent 未正常完成\n原因: ${errText}`;
1334
- console.warn(`[OpenClaw] recoverable lifecycle error for runId=${evRunId || runId}; deferring (agent may auto-recover): ${errText.slice(0, 120)}`);
1354
+ pendingRuntimeFailureText = `⚠️ Agent 未正常完成\n原因: ${safeErrorSummary(errText)}`;
1355
+ console.warn(`[OpenClaw] recoverable lifecycle error for runId=${evRunId || runId}; deferring (agent may auto-recover): ${safeErrorSummary(errText)}`);
1335
1356
  return;
1336
1357
  }
1337
1358
  if (isWaitTimeout(errText)) {
@@ -1387,8 +1408,8 @@ export class OpenClawClient {
1387
1408
  continue;
1388
1409
  }
1389
1410
  if (this.isRecoverableAgentError(errText)) {
1390
- pendingRuntimeFailureText = `⚠️ Agent 未正常完成\n原因: ${errText}`;
1391
- console.warn(`[OpenClaw] recoverable chat error for runId=${evRunId || runId}; deferring: ${errText.slice(0, 120)}`);
1411
+ pendingRuntimeFailureText = `⚠️ Agent 未正常完成\n原因: ${safeErrorSummary(errText)}`;
1412
+ console.warn(`[OpenClaw] recoverable chat error for runId=${evRunId || runId}; deferring: ${safeErrorSummary(errText)}`);
1392
1413
  bucket.splice(i, 1);
1393
1414
  continue;
1394
1415
  }
@@ -1412,7 +1433,7 @@ export class OpenClawClient {
1412
1433
  // reject, so an auto-retry probe does not race a zombie run on the
1413
1434
  // same session. Idempotent/best-effort.
1414
1435
  this.abortChat(targetSessionKey || sessionKey, evRunId || runId).catch((abortErr) => {
1415
- console.warn(`[OpenClaw] abort after chat error failed:`, abortErr.message);
1436
+ console.warn(`[OpenClaw] abort after chat error failed:`, safeErrorSummary(abortErr));
1416
1437
  });
1417
1438
  reject(new Error(`Agent error: ${errText}`));
1418
1439
  return;
@@ -2075,6 +2096,139 @@ export class OpenClawClient {
2075
2096
  if (next)
2076
2097
  next();
2077
2098
  }
2099
+ /** Public native Goal protocol. Completion deliberately unavailable without host revision CAS. */
2100
+ async createGoNativeGoal(p) {
2101
+ return this.rpc("chat.send", { sessionKey: this.canonicalSessionKey(p.sessionKey), sessionId: p.sessionId,
2102
+ message: p.objective, idempotencyKey: p.operationId, deliver: false,
2103
+ intent: { kind: "session-goal-start", version: 1, issuedAtMs: p.issuedAtMs } });
2104
+ }
2105
+ async readGoNativeGoal(sessionKey, sessionId) {
2106
+ const info = await this.getSessionInfo(sessionKey);
2107
+ if (info?.session?.sessionId !== sessionId || info?.session?.archived)
2108
+ throw new Error("Go session generation changed or archived");
2109
+ return { session: info.session, goal: info.session.goal ?? null, revisionCAS: "unsupported" };
2110
+ }
2111
+ async updateGoNativeGoal(p) {
2112
+ if (!["edit", "pause", "resume", "block"].includes(p.action))
2113
+ throw new Error("Native Goal completion CAS unsupported");
2114
+ const { objective, note, ...identity } = p;
2115
+ return this.rpc("sessions.goal.update", { ...identity, sessionKey: this.canonicalSessionKey(p.sessionKey),
2116
+ ...(p.action === "edit" ? { objective } : note === undefined ? {} : { note }) });
2117
+ }
2118
+ /** Admission receipt, not delivery. No restore, random retry key, or generation fallback. */
2119
+ async sendGoGuarded(p) {
2120
+ if (!p.sessionId || !p.idempotencyKey || p.expectedLeafEntryId === undefined)
2121
+ throw new Error("Missing Go send guards");
2122
+ // Own delivery before admission: events can beat the RPC response. Mute only
2123
+ // this brief admission window; afterwards exact run ownership avoids hiding
2124
+ // unrelated proactive work. A lost receipt retains the conservative fence.
2125
+ const release = this.muteProactiveDelivery(p.sessionKey);
2126
+ this.ownDeliveryRun(p.idempotencyKey);
2127
+ try {
2128
+ const result = await this.rpc("chat.send", { ...p, message: p.message + this.bridgeAttachmentHint(p.message),
2129
+ sessionKey: this.canonicalSessionKey(p.sessionKey), deliver: false, suppressCommandInterpretation: true });
2130
+ if (typeof result?.runId !== "string" || !result.runId)
2131
+ throw new Error("Unknown Go admission receipt");
2132
+ this.ownDeliveryRun(result.runId);
2133
+ return { runId: result.runId, sessionId: p.sessionId, idempotencyKey: p.idempotencyKey, raw: result };
2134
+ }
2135
+ finally {
2136
+ release();
2137
+ }
2138
+ }
2139
+ /** Checker collection accepts only the admitted run, never a session-key fallback. */
2140
+ async collectGoRun(p) {
2141
+ if (p.sessionId && (await this.getGoSessionInfo(p.sessionKey))?.session?.sessionId !== p.sessionId)
2142
+ throw new Error("Go result generation changed");
2143
+ const text = await this.collectReply(p.runId, p.timeoutMs, p.sessionKey, { exactRunIdentity: true });
2144
+ if (p.sessionId && (await this.getGoSessionInfo(p.sessionKey))?.session?.sessionId !== p.sessionId)
2145
+ throw new Error("Go result generation changed");
2146
+ return text;
2147
+ }
2148
+ /** Public committed terminalReply is the authority, including explicit empty
2149
+ * and terminal error. Stream prefaces, lifecycle end alone and timeout are not. */
2150
+ async readGoExecutorRunResult(p, waitMs = 1) {
2151
+ const before = await this.getGoSessionInfo(p.sessionKey);
2152
+ if (before?.session?.sessionId !== p.sessionId || before.session.archived)
2153
+ return null;
2154
+ this.ownDeliveryRun(p.runId);
2155
+ const result = await this.rpc("agent.wait", { runId: p.runId, timeoutMs: waitMs }, Math.max(2500, waitMs + 1500));
2156
+ const after = await this.getGoSessionInfo(p.sessionKey);
2157
+ if (after?.session?.sessionId !== p.sessionId || after.session.archived || result?.runId !== p.runId ||
2158
+ (result.sessionId !== undefined && result.sessionId !== p.sessionId) || !Number.isFinite(result.endedAt) || result.endedAt <= 0 ||
2159
+ (Number.isFinite(result.startedAt) && result.endedAt < result.startedAt) || result.yielded || result.pendingError)
2160
+ return null;
2161
+ const identity = { sessionKey: this.canonicalSessionKey(p.sessionKey), sessionId: p.sessionId, runId: p.runId };
2162
+ if (result.status === "error" || result.status === "timeout")
2163
+ return { ...identity, disposition: "error", text: "", error: String(result.error || `Run ended: ${result.status}`) };
2164
+ if (result.status !== "ok")
2165
+ return null;
2166
+ const reply = result.terminalReply;
2167
+ if (reply?.disposition === "silent")
2168
+ return { ...identity, disposition: "silent", text: "" };
2169
+ if (reply?.disposition === "empty")
2170
+ return { ...identity, disposition: "empty", text: "" };
2171
+ if (reply?.disposition !== "visible" || typeof reply.text !== "string")
2172
+ return null;
2173
+ return { ...identity, disposition: reply.text.trim() === "NO_REPLY" ? "silent" : reply.text.trim() ? "visible" : "empty", text: reply.text };
2174
+ }
2175
+ async collectGoExecutorRun(p) {
2176
+ const deadline = Date.now() + p.timeoutMs;
2177
+ do {
2178
+ const result = await this.readGoExecutorRunResult(p, Math.min(1500, Math.max(1, deadline - Date.now())));
2179
+ if (result)
2180
+ return result;
2181
+ if (Date.now() < deadline)
2182
+ await new Promise(r => setTimeout(r, 250));
2183
+ } while (Date.now() < deadline);
2184
+ throw new Error("Exact Go executor terminal result unknown; no replay");
2185
+ }
2186
+ async readGoRunResult(p) {
2187
+ const result = await this.readGoExecutorRunResult(p);
2188
+ return !result || result.disposition === "error" || result.disposition === "empty" ? null : result.disposition === "silent" ? "NO_REPLY" : result.text;
2189
+ }
2190
+ /** Public generation-pinned original input receipts; absence is unknown, not permission to resend. */
2191
+ async reconcileGoSend(p) {
2192
+ const result = await this.rpc("chat.history", { sessionKey: this.canonicalSessionKey(p.sessionKey),
2193
+ sessionId: p.sessionId, inputRunIds: [p.idempotencyKey], limit: 1 });
2194
+ const receipt = result?.inputReceipts?.find((r) => r.runId === p.idempotencyKey);
2195
+ return receipt ? { runId: receipt.runId, sessionId: p.sessionId, raw: receipt } : null;
2196
+ }
2197
+ async bindGoPolicy(binding) { return this.rpc("lma.go.bind", { binding }); }
2198
+ /** History publicly projects active leaf/run/recovery; describe alone does not. */
2199
+ async getGoSessionInfo(sessionKey) {
2200
+ const history = await this.rpc("chat.history", { sessionKey: this.canonicalSessionKey(sessionKey), limit: 1 });
2201
+ return { session: history?.sessionInfo };
2202
+ }
2203
+ async verifyGoSafety(_executorKey) {
2204
+ const capability = await this.rpc("lma.go.capabilities", {});
2205
+ return { checkerToolsIsolated: capability?.mode === "independent" && capability.checkerToolsIsolated === true,
2206
+ exactTaskAndDeliveryGate: capability?.taskQuery === true, guardedSend: true };
2207
+ }
2208
+ async initializeGoRuntime(p) {
2209
+ const binding = { checkerSessionKey: this.canonicalSessionKey(p.checkerSessionKey), checkerSessionId: p.checkerSessionId,
2210
+ executorSessionKey: this.canonicalSessionKey(p.executorSessionKey), executorSessionId: p.executorSessionId };
2211
+ const result = await this.bindGoPolicy(binding);
2212
+ if (result?.persisted !== true || JSON.stringify(result.binding) !== JSON.stringify(binding))
2213
+ throw new Error("Checker restriction binding not confirmed");
2214
+ return { executorSessionId: p.executorSessionId, checkerSessionId: p.checkerSessionId, checkerRunId: "", executorRunIds: [] };
2215
+ }
2216
+ async readGoRuntimeSnapshot(sessionKey, sessionId) {
2217
+ const key = this.canonicalSessionKey(sessionKey);
2218
+ const before = await this.getGoSessionInfo(key);
2219
+ const query = await this.rpc("lma.go.snapshot", { sessionKey: key, sessionId });
2220
+ const history = await this.rpc("chat.history", { sessionKey: key, sessionId, limit: 1 });
2221
+ const live = history?.sessionInfo;
2222
+ const after = await this.getGoSessionInfo(key);
2223
+ return { sessionKey: key, sessionId, archived: after?.session?.archived === true,
2224
+ complete: false, independent: true, atomicSnapshot: false,
2225
+ queryAvailable: query?.queryAvailable === true && query.sessionKey === key && query.requestedSessionId === sessionId && live?.sessionId === sessionId && before?.session?.sessionId === sessionId && after?.session?.sessionId === sessionId &&
2226
+ before.session.activeLeafEntryId === after.session.activeLeafEntryId && typeof live?.hasActiveRun === "boolean",
2227
+ running: history?.inFlightRun ? true : live?.hasActiveRun, activeRunIds: live?.activeRunIds,
2228
+ queuedInputs: history?.pendingInputs?.total,
2229
+ tasks: (query?.tasks ?? []).map((t) => ({ taskId: t.id, runId: t.runId ?? "", parentTaskId: t.parentTaskId ?? null,
2230
+ state: t.status === "succeeded" ? "completed" : t.status, deliveryState: t.deliveryStatus === "not_applicable" ? "not_required" : t.deliveryStatus })) };
2231
+ }
2078
2232
  async chatSend(params) {
2079
2233
  const sk = this.shortKey(params.sessionKey);
2080
2234
  const fullSessionKey = this.canonicalSessionKey(params.sessionKey);
@@ -0,0 +1,24 @@
1
+ import type { Locale } from "./i18n.js";
2
+ export type ErrorCategory = "cancelled" | "model" | "model_unavailable" | "model_policy" | "tools" | "context" | "compaction_failed" | "quota" | "rate_limit" | "auth" | "network" | "ttft_timeout" | "total_timeout" | "collector_timeout" | "timeout" | "unknown";
3
+ export interface SafeProviderError {
4
+ category: ErrorCategory;
5
+ status?: number;
6
+ code?: string;
7
+ type?: string;
8
+ param?: string;
9
+ requestId?: string;
10
+ safeMessage?: string;
11
+ }
12
+ /** Bounded parsing of SDK strings (e.g. Agent error: 400 {"error":...})
13
+ * and nested JSON errors. Deeper structured provider fields beat wrapper fields.
14
+ * Message text is used for classification, never returned verbatim. */
15
+ export declare function classifyProviderError(error: unknown): SafeProviderError;
16
+ /** Transparency (user directive 2026-10-09): the visible notice must always
17
+ * carry the original diagnostic so "unclassified" is still actionable. The
18
+ * excerpt is extracted from the error's diagnostic envelope fields and
19
+ * sanitized (no HTML, no URLs, no keys/tokens, capped) — never verbatim
20
+ * arbitrary provider prose. Kept out of classifyProviderError so the
21
+ * classification shape stays stable. */
22
+ export declare function rawErrorExcerpt(error: unknown): string | undefined;
23
+ export declare function safeErrorSummary(error: unknown): string;
24
+ export declare function formatProviderError(error: unknown, botName: string, model: string, locale: Locale): string;