Skip to content

Commit 55fa530

Browse files
author
OpenClaw-User
committed
fix(ui): i18n locale before render + fix: tool_use mismatch suggest /new (#46366, #46365)
1 parent 0c926a2 commit 55fa530

File tree

7 files changed

+68
-10
lines changed

7 files changed

+68
-10
lines changed

src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ describe("formatAssistantErrorText", () => {
6262
expect(result).toContain("Session history looks corrupted");
6363
expect(result).toContain("/new");
6464
});
65+
it("returns a recovery hint for tool_use_id mismatch errors", () => {
66+
const msg = makeAssistantError(
67+
'tool_result block with tool_use_id "toolu_abc" not found in the immediately preceding assistant message',
68+
);
69+
const result = formatAssistantErrorText(msg);
70+
expect(result).toContain("Conversation history corruption detected");
71+
expect(result).toContain("/new");
72+
});
73+
it("returns a recovery hint for unexpected tool_result block errors", () => {
74+
const msg = makeAssistantError(
75+
"unexpected tool_result block: no corresponding tool_use in the previous assistant turn",
76+
);
77+
const result = formatAssistantErrorText(msg);
78+
expect(result).toContain("Conversation history corruption detected");
79+
expect(result).toContain("/new");
80+
});
6581
it("handles JSON-wrapped role errors", () => {
6682
const msg = makeAssistantError('{"error":{"message":"400 Incorrect role information"}}');
6783
const result = formatAssistantErrorText(msg);

src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ describe("sanitizeUserFacingText", () => {
2727
expect(result).toContain("Message ordering conflict");
2828
});
2929

30+
it("sanitizes tool_use/tool_result mismatch errors", () => {
31+
const result = sanitizeUserFacingText(
32+
'tool_result block with tool_use_id "toolu_xyz" has no corresponding tool_use',
33+
{ errorContext: true },
34+
);
35+
expect(result).toContain("Conversation history corruption detected");
36+
expect(result).toContain("/new");
37+
});
38+
3039
it("sanitizes HTTP status errors with error hints", () => {
3140
expect(sanitizeUserFacingText("500 Internal Server Error", { errorContext: true })).toBe(
3241
"HTTP 500: Internal Server Error",

src/agents/pi-embedded-helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export {
3939
isRawApiErrorPayload,
4040
isRateLimitAssistantError,
4141
isRateLimitErrorMessage,
42+
isToolUseResultMismatchError,
4243
isTransientHttpError,
4344
isTimeoutErrorMessage,
4445
parseImageDimensionError,

src/agents/pi-embedded-helpers/errors.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,10 @@ export function formatAssistantErrorText(
732732
);
733733
}
734734

735+
if (isToolUseResultMismatchError(raw)) {
736+
return "Conversation history corruption detected. Please use /new to start a fresh session.";
737+
}
738+
735739
const invalidRequest = raw.match(/"type":"invalid_request_error".*?"message":"([^"]+)"/);
736740
if (invalidRequest?.[1]) {
737741
return `LLM request rejected: ${invalidRequest[1]}`;
@@ -782,6 +786,10 @@ export function sanitizeUserFacingText(text: string, opts?: { errorContext?: boo
782786
);
783787
}
784788

789+
if (isToolUseResultMismatchError(trimmed)) {
790+
return "Conversation history corruption detected. Please use /new to start a fresh session.";
791+
}
792+
785793
if (shouldRewriteContextOverflowText(trimmed)) {
786794
return (
787795
"Context overflow: prompt too large for the model. " +
@@ -827,6 +835,9 @@ const TOOL_CALL_INPUT_MISSING_RE =
827835
const TOOL_CALL_INPUT_PATH_RE =
828836
/messages\.\d+\.content\.\d+\.tool_(?:use|call)\.(?:input|arguments)/i;
829837

838+
const TOOL_USE_RESULT_MISMATCH_RE =
839+
/tool_use_id|tool_result.*corresponding.*tool_use|unexpected.*tool.*block/i;
840+
830841
const IMAGE_DIMENSION_ERROR_RE =
831842
/image dimensions exceed max allowed size for many-image requests:\s*(\d+)\s*pixels/i;
832843
const IMAGE_DIMENSION_PATH_RE = /messages\.(\d+)\.content\.(\d+)\.image/i;
@@ -839,6 +850,13 @@ export function isMissingToolCallInputError(raw: string): boolean {
839850
return TOOL_CALL_INPUT_MISSING_RE.test(raw) || TOOL_CALL_INPUT_PATH_RE.test(raw);
840851
}
841852

853+
export function isToolUseResultMismatchError(raw: string): boolean {
854+
if (!raw) {
855+
return false;
856+
}
857+
return TOOL_USE_RESULT_MISMATCH_RE.test(raw);
858+
}
859+
842860
export function isBillingAssistantError(msg: AssistantMessage | undefined): boolean {
843861
if (!msg || msg.stopReason !== "error") {
844862
return false;

ui/src/i18n/lib/translate.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ class I18nManager {
1717
private translations: Partial<Record<Locale, TranslationMap>> = { [DEFAULT_LOCALE]: en };
1818
private subscribers: Set<Subscriber> = new Set();
1919

20+
/** Resolves once the initial (persisted / navigator-derived) locale is loaded. */
21+
public readonly ready: Promise<void>;
22+
2023
constructor() {
21-
this.loadLocale();
24+
this.ready = this.loadLocale();
2225
}
2326

2427
private readStoredLocale(): string | null {
@@ -55,15 +58,15 @@ class I18nManager {
5558
return resolveNavigatorLocale(language ?? "");
5659
}
5760

58-
private loadLocale() {
61+
private async loadLocale(): Promise<void> {
5962
const initialLocale = this.resolveInitialLocale();
6063
if (initialLocale === DEFAULT_LOCALE) {
6164
this.locale = DEFAULT_LOCALE;
6265
return;
6366
}
6467
// Use the normal locale setter so startup locale loading follows the same
6568
// translation-loading + notify path as manual locale changes.
66-
void this.setLocale(initialLocale);
69+
await this.setLocale(initialLocale);
6770
}
6871

6972
public getLocale(): Locale {

ui/src/i18n/test/translate.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,7 @@ describe("i18n", () => {
8585
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
8686
localStorage.setItem("openclaw.i18n.locale", "zh-CN");
8787
const fresh = await import("../lib/translate.ts");
88-
await vi.waitFor(() => {
89-
expect(fresh.i18n.getLocale()).toBe("zh-CN");
90-
});
88+
await fresh.i18n.ready;
9189
expect(fresh.i18n.getLocale()).toBe("zh-CN");
9290
expect(fresh.t("common.health")).toBe("健康状况");
9391
});

ui/src/ui/app.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LitElement } from "lit";
1+
import { LitElement, nothing } from "lit";
22
import { customElement, state } from "lit/decorators.js";
33
import { i18n, I18nController, isSupportedLocale } from "../i18n/index.ts";
44
import {
@@ -115,11 +115,21 @@ export class OpenClawApp extends LitElement {
115115
clientInstanceId = generateUUID();
116116
connectGeneration = 0;
117117
@state() settings: UiSettings = loadSettings();
118+
@state() private localeReady = false;
118119
constructor() {
119120
super();
120-
if (isSupportedLocale(this.settings.locale)) {
121-
void i18n.setLocale(this.settings.locale);
122-
}
121+
const settingsLocale = isSupportedLocale(this.settings.locale)
122+
? i18n.setLocale(this.settings.locale)
123+
: Promise.resolve();
124+
Promise.all([i18n.ready, settingsLocale]).then(
125+
() => {
126+
this.localeReady = true;
127+
},
128+
() => {
129+
// Locale load failed; render with the default locale.
130+
this.localeReady = true;
131+
},
132+
);
123133
}
124134
@state() password = "";
125135
@state() loginShowGatewayToken = false;
@@ -716,6 +726,9 @@ export class OpenClawApp extends LitElement {
716726
}
717727

718728
render() {
729+
if (!this.localeReady) {
730+
return nothing;
731+
}
719732
return renderApp(this as unknown as AppViewState);
720733
}
721734
}

0 commit comments

Comments
 (0)