openclaw-lark-multi-agent 1.4.9 → 1.4.10
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/feishu-bot.js +4 -1
- package/dist/openclaw-client.js +75 -6
- package/dist/session-status.d.ts +3 -2
- package/dist/session-status.js +9 -5
- package/package.json +1 -1
package/dist/feishu-bot.js
CHANGED
|
@@ -1388,7 +1388,10 @@ export class FeishuBot {
|
|
|
1388
1388
|
onProgress: (event) => liveStatus?.progress(event),
|
|
1389
1389
|
onWaitPaused: async (notice) => {
|
|
1390
1390
|
await liveStatus?.showWaitingForResult(notice.message);
|
|
1391
|
-
|
|
1391
|
+
// Yield is a confirmed handoff, not the timeout heuristic. A
|
|
1392
|
+
// recent unrelated reply must not suppress this turn's notice.
|
|
1393
|
+
if (notice.reason === "yield" ? !this.store.hasDeliveredReply(this.config.name, chatId, triggerId)
|
|
1394
|
+
: Date.now() - (this.lastRealDeliveryAt.get(chatId) || 0) >= 5000)
|
|
1392
1395
|
await this.sendWaitPaused(chatId, triggerId, lastHuman.messageId, notice);
|
|
1393
1396
|
},
|
|
1394
1397
|
}));
|
package/dist/openclaw-client.js
CHANGED
|
@@ -231,7 +231,9 @@ export class OpenClawClient {
|
|
|
231
231
|
this.agentEvents.get(sk).push({
|
|
232
232
|
...frame.payload,
|
|
233
233
|
stream: "chatFinal",
|
|
234
|
-
|
|
234
|
+
// Only the Gateway chat-final envelope confirms a handoff.
|
|
235
|
+
// Never infer it from tool names, arguments or raw tool output.
|
|
236
|
+
data: { text: textParts.join("\n"), gatewayYielded: frame.payload?.yielded === true },
|
|
235
237
|
});
|
|
236
238
|
}
|
|
237
239
|
else if (state === "error") {
|
|
@@ -467,6 +469,11 @@ export class OpenClawClient {
|
|
|
467
469
|
/** Render protocol-4 structured tool args/results without `[object Object]`.
|
|
468
470
|
* Prefer useful, non-secret fields and keep the fallback bounded. */
|
|
469
471
|
formatToolValue(value, toolName, phase) {
|
|
472
|
+
// sessions_yield.message is private resumed context. Do not mirror its
|
|
473
|
+
// arguments/results (including JSON strings) into process cards or verbose
|
|
474
|
+
// tool replies. This is redaction only, never evidence of a handoff.
|
|
475
|
+
if (toolName === "sessions_yield")
|
|
476
|
+
return "";
|
|
470
477
|
if (value === null || value === undefined || value === "")
|
|
471
478
|
return "";
|
|
472
479
|
if (typeof value === "string")
|
|
@@ -704,7 +711,7 @@ export class OpenClawClient {
|
|
|
704
711
|
*/
|
|
705
712
|
collectReply(runId, timeoutMs = FOREGROUND_WAIT_MS, targetSessionKey, options) {
|
|
706
713
|
let cleanupObservation = () => { };
|
|
707
|
-
return new Promise((
|
|
714
|
+
return new Promise((resolveResult, rejectResult) => {
|
|
708
715
|
// Whether we were connected when the run started. We only treat a later
|
|
709
716
|
// disconnect as a mid-run gateway drop (fail fast) if we began connected;
|
|
710
717
|
// this keeps unit tests that drive collectReply without a live socket from
|
|
@@ -713,6 +720,7 @@ export class OpenClawClient {
|
|
|
713
720
|
let text = "";
|
|
714
721
|
let chatDeltaText = "";
|
|
715
722
|
let chatFinalText = "";
|
|
723
|
+
let chatFinalSeen = false;
|
|
716
724
|
let transcriptAssistantText = "";
|
|
717
725
|
let sessionKey = targetSessionKey ? `agent:main:${targetSessionKey.replace(/^agent:[^:]+:/, "")}` : "";
|
|
718
726
|
let shortSessionKey = targetSessionKey ? targetSessionKey.replace(/^agent:[^:]+:/, "") : "";
|
|
@@ -739,12 +747,18 @@ export class OpenClawClient {
|
|
|
739
747
|
};
|
|
740
748
|
let idleTimer;
|
|
741
749
|
let observationClosed = false;
|
|
750
|
+
// Close synchronously: a deferred yield notice must not overtake a final
|
|
751
|
+
// or error just because Promise.finally cleanup has not run yet.
|
|
752
|
+
const resolve = (value) => { observationClosed = true; resolveResult(value); };
|
|
753
|
+
const reject = (error) => { observationClosed = true; rejectResult(error); };
|
|
742
754
|
let checkingState = false;
|
|
743
755
|
let lastStateCheckAt = -Infinity;
|
|
744
756
|
let quietNotified = false;
|
|
745
757
|
let decidingTimeout = false;
|
|
746
758
|
let waitNoticeSent = false;
|
|
747
759
|
let foregroundPaused = false;
|
|
760
|
+
let foregroundYielded = false;
|
|
761
|
+
let turnYielded = false;
|
|
748
762
|
let foregroundWaitTimer;
|
|
749
763
|
const handleTimeoutResult = async (error) => {
|
|
750
764
|
if (observationClosed)
|
|
@@ -982,6 +996,16 @@ export class OpenClawClient {
|
|
|
982
996
|
}
|
|
983
997
|
continue;
|
|
984
998
|
}
|
|
999
|
+
// After handoff, an unowned new run (including requester-settle)
|
|
1000
|
+
// retains its existing proactive delivery route. A session/lifecycle
|
|
1001
|
+
// start alone must not turn it into the old run's final. Discussion's
|
|
1002
|
+
// transcript mute marks scheduler-owned collection; retain its existing
|
|
1003
|
+
// anchored collector path rather than changing discussion routing.
|
|
1004
|
+
if (eventSessionMatches && foregroundYielded && evRunId && !matchesRun
|
|
1005
|
+
&& !this.mutedProactiveSessions.has(sessionKey) && !this.mutedProactiveSessions.has(shortSessionKey)) {
|
|
1006
|
+
bucket.splice(i, 1);
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
985
1009
|
if (eventSessionMatches && expectedUserText && !anchorSeen && !matchesRun) {
|
|
986
1010
|
bucket.splice(i, 1);
|
|
987
1011
|
preAnchorEvents.push(ev);
|
|
@@ -1035,6 +1059,10 @@ export class OpenClawClient {
|
|
|
1035
1059
|
if (ev.stream !== "lifecycle") {
|
|
1036
1060
|
pendingRuntimeFailureText = "";
|
|
1037
1061
|
}
|
|
1062
|
+
// A matched resumed turn can again complete via the existing
|
|
1063
|
+
// assistant/transcript + lifecycle path, even without chat final.
|
|
1064
|
+
if (ev.stream === "lifecycle" && ev.data?.phase === "start")
|
|
1065
|
+
turnYielded = false;
|
|
1038
1066
|
if (ev.stream === "lifecycle" && ev.data?.phase === "start" && !lifecycleStartedLogged) {
|
|
1039
1067
|
lifecycleStartedLogged = true;
|
|
1040
1068
|
console.log(`[OpenClaw] lifecycle start for runId=${runId} after ${Date.now() - collectStartedAt}ms`);
|
|
@@ -1067,17 +1095,48 @@ export class OpenClawClient {
|
|
|
1067
1095
|
}
|
|
1068
1096
|
}
|
|
1069
1097
|
if (ev.stream === "chatFinal") {
|
|
1098
|
+
chatFinalSeen = true;
|
|
1099
|
+
if (ev.data?.gatewayYielded === true) {
|
|
1100
|
+
// A yielded turn is not a terminal answer, even if its buffered
|
|
1101
|
+
// message contains a preface. Preserve the collector/queue owner
|
|
1102
|
+
// and final-delivery key; only pause foreground presentation.
|
|
1103
|
+
if (chatFinalText.trim())
|
|
1104
|
+
continue; // an actual final wins
|
|
1105
|
+
foregroundYielded = true;
|
|
1106
|
+
turnYielded = true;
|
|
1107
|
+
text = chatDeltaText = transcriptAssistantText = "";
|
|
1108
|
+
if (chatFinalTimer) {
|
|
1109
|
+
clearTimeout(chatFinalTimer);
|
|
1110
|
+
chatFinalTimer = null;
|
|
1111
|
+
}
|
|
1112
|
+
if (lifecycleEndTimer) {
|
|
1113
|
+
clearTimeout(lifecycleEndTimer);
|
|
1114
|
+
lifecycleEndTimer = null;
|
|
1115
|
+
}
|
|
1116
|
+
// Finish scanning this batch first so an already-arrived real
|
|
1117
|
+
// final/error wins over a stale waiting notice. No status RPC
|
|
1118
|
+
// (or ten-minute budget) is needed to confirm this wire fact.
|
|
1119
|
+
if (options?.onWaitPaused)
|
|
1120
|
+
void Promise.resolve().then(() => handleTimeoutResult(new SessionWaitPaused(normalizeSessionRuntimeStatus(undefined), false, "yield")));
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1070
1123
|
chatFinalText = ev.data?.text || "";
|
|
1124
|
+
if (chatFinalText.trim())
|
|
1125
|
+
turnYielded = false;
|
|
1126
|
+
else if (turnYielded)
|
|
1127
|
+
continue;
|
|
1071
1128
|
// Fallback: if lifecycle end doesn't arrive within 5s, resolve
|
|
1072
1129
|
if (!chatFinalTimer) {
|
|
1073
1130
|
chatFinalTimer = setTimeout(() => {
|
|
1074
1131
|
console.warn(`[OpenClaw] collectReply: lifecycle end missing, using chatFinal fallback`);
|
|
1075
|
-
this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
|
|
1076
|
-
console.warn(`[OpenClaw] abort after chatFinal fallback failed:`, err.message);
|
|
1077
|
-
});
|
|
1078
1132
|
// Prefer final chat message over accumulated deltas: some providers may
|
|
1079
1133
|
// emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
|
|
1080
1134
|
const latestFinalText = this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
|
|
1135
|
+
// Never abort a yielded original run, nor an empty final.
|
|
1136
|
+
if (!foregroundYielded && latestFinalText)
|
|
1137
|
+
this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
|
|
1138
|
+
console.warn(`[OpenClaw] abort after chatFinal fallback failed:`, err.message);
|
|
1139
|
+
});
|
|
1081
1140
|
if (latestFinalText) {
|
|
1082
1141
|
finish(latestFinalText);
|
|
1083
1142
|
}
|
|
@@ -1092,6 +1151,11 @@ export class OpenClawClient {
|
|
|
1092
1151
|
}
|
|
1093
1152
|
}
|
|
1094
1153
|
if (ev.stream === "lifecycle" && ev.data?.phase === "end") {
|
|
1154
|
+
// paused/end_turn can beat the confirming chat event, including
|
|
1155
|
+
// discussion's emptyFinalAsNoReply path. It is neither success
|
|
1156
|
+
// nor failure and alone does NOT authorize a handoff notice.
|
|
1157
|
+
if (!chatFinalText.trim() && (turnYielded || (ev.data?.livenessState === "paused" && ev.data?.stopReason === "end_turn")))
|
|
1158
|
+
continue;
|
|
1095
1159
|
// Prefer final chat message over accumulated deltas: some providers may
|
|
1096
1160
|
// emit only partial deltas (e.g. "N") while final contains "NO_REPLY".
|
|
1097
1161
|
const finalText = this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
|
|
@@ -1143,7 +1207,12 @@ export class OpenClawClient {
|
|
|
1143
1207
|
};
|
|
1144
1208
|
// If lifecycle end beats chat final, a short delta like "N" can be a truncated
|
|
1145
1209
|
// final reply. Wait for chatFinal before resolving; otherwise suppress lone "N".
|
|
1146
|
-
if (!options?.emptyFinalAsNoReply && ev.data?.livenessState === "working" && !chatFinalText && text.length <= 1)
|
|
1210
|
+
if ((!options?.emptyFinalAsNoReply && ev.data?.livenessState === "working" && !chatFinalText && text.length <= 1)
|
|
1211
|
+
|| (options?.emptyFinalAsNoReply && !chatFinalSeen && !finalText)) {
|
|
1212
|
+
// A lifecycle-only empty discussion end also needs this small
|
|
1213
|
+
// window for a confirming yielded chat final, not instant NO_REPLY.
|
|
1214
|
+
if (lifecycleEndTimer)
|
|
1215
|
+
clearTimeout(lifecycleEndTimer);
|
|
1147
1216
|
lifecycleEndTimer = setTimeout(finishFromLifecycle, 5000);
|
|
1148
1217
|
}
|
|
1149
1218
|
else {
|
package/dist/session-status.d.ts
CHANGED
|
@@ -11,8 +11,9 @@ export declare const isWaitTimeout: (text: string) => boolean;
|
|
|
11
11
|
export declare function normalizeSessionRuntimeStatus(value: any): SessionRuntimeStatus;
|
|
12
12
|
/** Compact presentation only; never replaces the actual status/model values. */
|
|
13
13
|
export declare function formatSessionFooter(snapshot: SessionRuntimeStatus): string;
|
|
14
|
-
/**
|
|
14
|
+
/** Foreground waiting paused, NOT an assertion that execution ended. */
|
|
15
15
|
export declare class SessionWaitPaused extends Error {
|
|
16
16
|
readonly snapshot: SessionRuntimeStatus;
|
|
17
|
-
|
|
17
|
+
readonly reason: "timeout" | "yield";
|
|
18
|
+
constructor(snapshot: SessionRuntimeStatus, reportedTimeout?: boolean, reason?: "timeout" | "yield");
|
|
18
19
|
}
|
package/dist/session-status.js
CHANGED
|
@@ -26,14 +26,18 @@ export function formatSessionFooter(snapshot) {
|
|
|
26
26
|
const limitK = limit === undefined ? "?" : Math.max(1, Math.round(limit / 1000));
|
|
27
27
|
return `${snapshot.status} · ${usedK}K/${limitK}K`;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
29
|
+
/** Foreground waiting paused, NOT an assertion that execution ended. */
|
|
30
30
|
export class SessionWaitPaused extends Error {
|
|
31
31
|
snapshot;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
reason;
|
|
33
|
+
constructor(snapshot, reportedTimeout = false, reason = "timeout") {
|
|
34
|
+
super(reason === "yield"
|
|
35
|
+
? "⏳ 本轮已转入后台等待;若有后续结果,将继续在此回复。未发送停止请求,也未自动重发请求。"
|
|
36
|
+
: reportedTimeout
|
|
37
|
+
? "⏳ 本轮返回了超时,LMA 已暂停本次等待,但不据此认定整个会话已经结束。会话状态见下方;你可以继续等待后续结果。"
|
|
38
|
+
: "⏳ 本次等待时间已到,LMA 暂停本轮等待,先返回会话状态供你判断。\n没有因此停止运行或自动重发本次请求;你可以继续等待后续结果。");
|
|
36
39
|
this.snapshot = snapshot;
|
|
40
|
+
this.reason = reason;
|
|
37
41
|
this.name = "SessionWaitPaused";
|
|
38
42
|
}
|
|
39
43
|
}
|