mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 19:04:58 +00:00
Merge branch 'main' of https://github.com/openclaw/openclaw
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- CI: Implement pipeline and workflow order. Thanks @quotentiroler.
|
||||||
- WhatsApp: preserve original filenames for inbound documents. (#12691) Thanks @akramcodez.
|
- WhatsApp: preserve original filenames for inbound documents. (#12691) Thanks @akramcodez.
|
||||||
- Telegram: harden quote parsing; preserve quote context; avoid QUOTE_TEXT_INVALID; avoid nested reply quote misclassification. (#12156) Thanks @rybnikov.
|
- Telegram: harden quote parsing; preserve quote context; avoid QUOTE_TEXT_INVALID; avoid nested reply quote misclassification. (#12156) Thanks @rybnikov.
|
||||||
- Telegram: recover proactive sends when stale topic thread IDs are used by retrying without `message_thread_id`. (#11620)
|
- Telegram: recover proactive sends when stale topic thread IDs are used by retrying without `message_thread_id`. (#11620)
|
||||||
@@ -30,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Tools/web_search: normalize direct Perplexity model IDs while keeping OpenRouter model IDs unchanged. (#12795) Thanks @cdorsey.
|
- Tools/web_search: normalize direct Perplexity model IDs while keeping OpenRouter model IDs unchanged. (#12795) Thanks @cdorsey.
|
||||||
- Model failover: treat HTTP 400 errors as failover-eligible, enabling automatic model fallback. (#1879) Thanks @orenyomtov.
|
- Model failover: treat HTTP 400 errors as failover-eligible, enabling automatic model fallback. (#1879) Thanks @orenyomtov.
|
||||||
- Errors: prevent false positive context overflow detection when conversation mentions "context overflow" topic. (#2078) Thanks @sbking.
|
- Errors: prevent false positive context overflow detection when conversation mentions "context overflow" topic. (#2078) Thanks @sbking.
|
||||||
|
- Errors: avoid rewriting/swallowing normal assistant replies that mention error keywords by scoping `sanitizeUserFacingText` rewrites to error-context. (#12988) Thanks @Takhoffman.
|
||||||
- Config: re-hydrate state-dir `.env` during runtime config loads so `${VAR}` substitutions remain resolvable. (#12748) Thanks @rodrigouroz.
|
- Config: re-hydrate state-dir `.env` during runtime config loads so `${VAR}` substitutions remain resolvable. (#12748) Thanks @rodrigouroz.
|
||||||
- Gateway: no more post-compaction amnesia; injected transcript writes now preserve Pi session `parentId` chain so agents can remember again. (#12283) Thanks @Takhoffman.
|
- Gateway: no more post-compaction amnesia; injected transcript writes now preserve Pi session `parentId` chain so agents can remember again. (#12283) Thanks @Takhoffman.
|
||||||
- Gateway: fix multi-agent sessions.usage discovery. (#11523) Thanks @Takhoffman.
|
- Gateway: fix multi-agent sessions.usage discovery. (#11523) Thanks @Takhoffman.
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ describe("sanitizeUserFacingText", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes role ordering errors", () => {
|
it("sanitizes role ordering errors", () => {
|
||||||
const result = sanitizeUserFacingText("400 Incorrect role information");
|
const result = sanitizeUserFacingText("400 Incorrect role information", { errorContext: true });
|
||||||
expect(result).toContain("Message ordering conflict");
|
expect(result).toContain("Message ordering conflict");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sanitizes HTTP status errors with error hints", () => {
|
it("sanitizes HTTP status errors with error hints", () => {
|
||||||
expect(sanitizeUserFacingText("500 Internal Server Error")).toBe(
|
expect(sanitizeUserFacingText("500 Internal Server Error", { errorContext: true })).toBe(
|
||||||
"HTTP 500: Internal Server Error",
|
"HTTP 500: Internal Server Error",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -27,11 +27,18 @@ describe("sanitizeUserFacingText", () => {
|
|||||||
expect(
|
expect(
|
||||||
sanitizeUserFacingText(
|
sanitizeUserFacingText(
|
||||||
"Context overflow: prompt too large for the model. Try again with less input or a larger-context model.",
|
"Context overflow: prompt too large for the model. Try again with less input or a larger-context model.",
|
||||||
|
{ errorContext: true },
|
||||||
),
|
),
|
||||||
).toContain("Context overflow: prompt too large for the model.");
|
).toContain("Context overflow: prompt too large for the model.");
|
||||||
expect(sanitizeUserFacingText("Request size exceeds model context window")).toContain(
|
expect(
|
||||||
"Context overflow: prompt too large for the model.",
|
sanitizeUserFacingText("Request size exceeds model context window", { errorContext: true }),
|
||||||
);
|
).toContain("Context overflow: prompt too large for the model.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not swallow assistant text that quotes the canonical context-overflow string", () => {
|
||||||
|
const text =
|
||||||
|
"Changelog note: we fixed false positives for `Context overflow: prompt too large for the model. Try again with less input or a larger-context model.` in 2026.2.9";
|
||||||
|
expect(sanitizeUserFacingText(text)).toBe(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not rewrite conversational mentions of context overflow", () => {
|
it("does not rewrite conversational mentions of context overflow", () => {
|
||||||
@@ -48,7 +55,9 @@ describe("sanitizeUserFacingText", () => {
|
|||||||
|
|
||||||
it("sanitizes raw API error payloads", () => {
|
it("sanitizes raw API error payloads", () => {
|
||||||
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}';
|
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}';
|
||||||
expect(sanitizeUserFacingText(raw)).toBe("LLM error server_error: Something exploded");
|
expect(sanitizeUserFacingText(raw, { errorContext: true })).toBe(
|
||||||
|
"LLM error server_error: Something exploded",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("collapses consecutive duplicate paragraphs", () => {
|
it("collapses consecutive duplicate paragraphs", () => {
|
||||||
|
|||||||
@@ -402,46 +402,51 @@ export function formatAssistantErrorText(
|
|||||||
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
|
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeUserFacingText(text: string): string {
|
export function sanitizeUserFacingText(text: string, opts?: { errorContext?: boolean }): string {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
const errorContext = opts?.errorContext ?? false;
|
||||||
const stripped = stripFinalTagsFromText(text);
|
const stripped = stripFinalTagsFromText(text);
|
||||||
const trimmed = stripped.trim();
|
const trimmed = stripped.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return stripped;
|
return stripped;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/incorrect role information|roles must alternate/i.test(trimmed)) {
|
// Only apply error-pattern rewrites when the caller knows this text is an error payload.
|
||||||
return (
|
// Otherwise we risk swallowing legitimate assistant text that merely *mentions* these errors.
|
||||||
"Message ordering conflict - please try again. " +
|
if (errorContext) {
|
||||||
"If this persists, use /new to start a fresh session."
|
if (/incorrect role information|roles must alternate/i.test(trimmed)) {
|
||||||
);
|
return (
|
||||||
}
|
"Message ordering conflict - please try again. " +
|
||||||
|
"If this persists, use /new to start a fresh session."
|
||||||
if (shouldRewriteContextOverflowText(trimmed)) {
|
);
|
||||||
return (
|
|
||||||
"Context overflow: prompt too large for the model. " +
|
|
||||||
"Try again with less input or a larger-context model."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBillingErrorMessage(trimmed)) {
|
|
||||||
return BILLING_ERROR_USER_MESSAGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRawApiErrorPayload(trimmed) || isLikelyHttpErrorText(trimmed)) {
|
|
||||||
return formatRawAssistantErrorForUi(trimmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ERROR_PREFIX_RE.test(trimmed)) {
|
|
||||||
if (isOverloadedErrorMessage(trimmed) || isRateLimitErrorMessage(trimmed)) {
|
|
||||||
return "The AI service is temporarily overloaded. Please try again in a moment.";
|
|
||||||
}
|
}
|
||||||
if (isTimeoutErrorMessage(trimmed)) {
|
|
||||||
return "LLM request timed out.";
|
if (shouldRewriteContextOverflowText(trimmed)) {
|
||||||
|
return (
|
||||||
|
"Context overflow: prompt too large for the model. " +
|
||||||
|
"Try again with less input or a larger-context model."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBillingErrorMessage(trimmed)) {
|
||||||
|
return BILLING_ERROR_USER_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRawApiErrorPayload(trimmed) || isLikelyHttpErrorText(trimmed)) {
|
||||||
|
return formatRawAssistantErrorForUi(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ERROR_PREFIX_RE.test(trimmed)) {
|
||||||
|
if (isOverloadedErrorMessage(trimmed) || isRateLimitErrorMessage(trimmed)) {
|
||||||
|
return "The AI service is temporarily overloaded. Please try again in a moment.";
|
||||||
|
}
|
||||||
|
if (isTimeoutErrorMessage(trimmed)) {
|
||||||
|
return "LLM request timed out.";
|
||||||
|
}
|
||||||
|
return formatRawAssistantErrorForUi(trimmed);
|
||||||
}
|
}
|
||||||
return formatRawAssistantErrorForUi(trimmed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return collapseConsecutiveDuplicateBlocks(stripped);
|
return collapseConsecutiveDuplicateBlocks(stripped);
|
||||||
|
|||||||
@@ -75,6 +75,19 @@ describe("extractAssistantText", () => {
|
|||||||
expect(result).toBe("This is a normal response without any tool calls.");
|
expect(result).toBe("This is a normal response without any tool calls.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sanitizes HTTP-ish error text only when stopReason is error", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
stopReason: "error",
|
||||||
|
errorMessage: "500 Internal Server Error",
|
||||||
|
content: [{ type: "text", text: "500 Internal Server Error" }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("HTTP 500: Internal Server Error");
|
||||||
|
});
|
||||||
|
|
||||||
it("strips Minimax tool invocations with extra attributes", () => {
|
it("strips Minimax tool invocations with extra attributes", () => {
|
||||||
const msg: AssistantMessage = {
|
const msg: AssistantMessage = {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
|
|||||||
@@ -218,7 +218,10 @@ export function extractAssistantText(msg: AssistantMessage): string {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
const extracted = blocks.join("\n").trim();
|
const extracted = blocks.join("\n").trim();
|
||||||
return sanitizeUserFacingText(extracted);
|
// Only apply keyword-based error rewrites when the assistant message is actually an error.
|
||||||
|
// Otherwise normal prose that *mentions* errors (e.g. "context overflow") can get clobbered.
|
||||||
|
const errorContext = msg.stopReason === "error" || Boolean(msg.errorMessage?.trim());
|
||||||
|
return sanitizeUserFacingText(extracted, { errorContext });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractAssistantThinking(msg: AssistantMessage): string {
|
export function extractAssistantThinking(msg: AssistantMessage): string {
|
||||||
|
|||||||
@@ -30,4 +30,14 @@ describe("extractAssistantText", () => {
|
|||||||
};
|
};
|
||||||
expect(extractAssistantText(message)).toBe("Hi there");
|
expect(extractAssistantText(message)).toBe("Hi there");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rewrites error-ish assistant text only when the transcript marks it as an error", () => {
|
||||||
|
const message = {
|
||||||
|
role: "assistant",
|
||||||
|
stopReason: "error",
|
||||||
|
errorMessage: "500 Internal Server Error",
|
||||||
|
content: [{ type: "text", text: "500 Internal Server Error" }],
|
||||||
|
};
|
||||||
|
expect(extractAssistantText(message)).toBe("HTTP 500: Internal Server Error");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -389,5 +389,10 @@ export function extractAssistantText(message: unknown): string | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const joined = chunks.join("").trim();
|
const joined = chunks.join("").trim();
|
||||||
return joined ? sanitizeUserFacingText(joined) : undefined;
|
const stopReason = (message as { stopReason?: unknown }).stopReason;
|
||||||
|
const errorMessage = (message as { errorMessage?: unknown }).errorMessage;
|
||||||
|
const errorContext =
|
||||||
|
stopReason === "error" || (typeof errorMessage === "string" && Boolean(errorMessage.trim()));
|
||||||
|
|
||||||
|
return joined ? sanitizeUserFacingText(joined, { errorContext }) : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,9 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
return { skip: true };
|
return { skip: true };
|
||||||
}
|
}
|
||||||
const sanitized = sanitizeUserFacingText(text);
|
const sanitized = sanitizeUserFacingText(text, {
|
||||||
|
errorContext: Boolean(payload.isError),
|
||||||
|
});
|
||||||
if (!sanitized.trim()) {
|
if (!sanitized.trim()) {
|
||||||
return { skip: true };
|
return { skip: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function normalizeReplyPayload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
text = sanitizeUserFacingText(text);
|
text = sanitizeUserFacingText(text, { errorContext: Boolean(payload.isError) });
|
||||||
}
|
}
|
||||||
if (!text?.trim() && !hasMedia && !hasChannelData) {
|
if (!text?.trim() && !hasMedia && !hasChannelData) {
|
||||||
opts.onSkip?.("empty");
|
opts.onSkip?.("empty");
|
||||||
|
|||||||
Reference in New Issue
Block a user