Skip to content

Commit 181076e

Browse files
authored
Merge branch 'main' into fix/mimo-reasoning-content
2 parents 3dbefe6 + 695a4f5 commit 181076e

96 files changed

Lines changed: 1893 additions & 257 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ Docs: https://docs.openclaw.ai
3232

3333
### Fixes
3434

35+
- Plugin skills: replace generated Windows plugin-skill directories before publishing the current skill link, avoiding repeated `EINVAL` warnings from stale non-symlink entries. Fixes #81432. (#81446) Thanks @hclsys and @vincentkoc.
36+
- Channels/config: treat channel entries with only `enabled: true` as configured state so plugin-backed channels can auto-enable from an explicit on switch. Fixes #81323. (#81331) Thanks @EvanYao826 and @vincentkoc.
37+
- CLI/update: add an update finalization path for externally swapped core runtimes, running update-time doctor repair and plugin convergence from post-doctor config and install-record state before reporting completion. Thanks @shakkernerd.
38+
- Agents/WebChat: stop a successful assistant turn whose stale `errorMessage` matches a billing, auth, or rate-limit pattern from rotating profiles, falling back, or surfacing a hard `FailoverError` unless the current attempt has a real failover failure. (#70900) Thanks @truffle-dev.
3539
- Control UI/logs: make the Gateway Logs stream height responsive to the viewport with a minimum height floor, so larger screens can show substantially more log lines without collapsing on shorter viewports. (#53916) Thanks @extrasmall0.
3640
- ACP/Codex: surface redacted Codex wrapper stderr for generic ACP internal failures and preserve safe Codex model/provider routing in isolated `CODEX_HOME`, making `sessions_spawn(runtime="acp", agentId="codex")` failures actionable. Fixes #80079. (#80718) Thanks @leoge007.
3741
- ACP: treat rejected timeout config options as best-effort hints so ACP turns continue with adapters that do not support `session/set_config_option` timeout keys. Fixes #81250. (#81603) Thanks @qkal.
@@ -46,6 +50,7 @@ Docs: https://docs.openclaw.ai
4650
- Discord: honor `threadName` on `message send` to existing threads by renaming the thread after successful delivery, and warn when the rename cannot be applied. Fixes #81836. (#81933) Thanks @joshavant.
4751
- Build: keep externalized Slack, OpenShell sandbox, and Anthropic Vertex runtime dependency declarations out of the root dist artifact build.
4852
- ClawHub: include Amazon Bedrock and Bedrock Mantle provider packages in the published registry metadata so the externalized providers are discoverable from ClawHub as well as npm.
53+
- Codex account/status: hide empty rate-limit buckets and show server-reported usage-limit blocks without calling them available.
4954
- Auto-reply/Claude CLI: bridge CLI-runtime assistant text-delta agent events into the chat reasoning preview through `onReasoningStream`, mirroring the existing assistant-text (#76914) and tool-event (#80046) bridges and adding gating so non-CLI runtimes are unaffected. Thanks @anagnorisis2peripeteia and @pashpashpash.
5055
- Mantis: keep QA evidence in Actions artifacts only and stop publishing evidence files to Git-backed artifact branches.
5156
- CLI/migrate: handle delayed Codex plugin marketplace responses so warnings, next-steps, and conflict states render with ⚠️ glyphs and post-install migration retries the marketplace fetch instead of silently skipping plugin items. (#81625) Thanks @sjf.
@@ -76,6 +81,20 @@ Docs: https://docs.openclaw.ai
7681
- Telnyx voice-call: use the raw `client_state` fallback when webhook state is malformed base64 instead of using silently corrupted decoded text.
7782
- Google Meet: report malformed node-host params JSON with plugin-owned errors instead of leaking raw JSON parser failures.
7883
- CLI/export-trajectory: report malformed encoded request JSON with a stable CLI error instead of leaking raw parser output.
84+
- ComfyUI: report malformed workflow API JSON responses with owned errors instead of leaking raw parser failures.
85+
- DeepInfra video: report malformed successful API JSON responses with provider-owned errors instead of leaking raw parser failures.
86+
- Brave Search: report malformed web and LLM-context API JSON with provider-owned errors instead of leaking raw parser failures.
87+
- xAI tools: report malformed web search, X search, and code execution JSON with provider-owned errors instead of leaking raw parser failures.
88+
- Nextcloud Talk: report malformed room-info and bot-admin JSON with channel-owned errors instead of leaking raw parser failures.
89+
- Microsoft Teams: report malformed Graph and delegated OAuth JSON with channel-owned errors instead of leaking raw parser failures.
90+
- Google Chat: report malformed Chat API and certificate JSON with channel-owned errors instead of leaking raw parser failures.
91+
- Firecrawl: report malformed search and scrape API JSON with provider-owned errors instead of leaking raw parser failures.
92+
- Tavily: report malformed search and extract API JSON with provider-owned errors instead of leaking raw parser failures.
93+
- Perplexity: report malformed Search API and chat completion JSON with provider-owned errors instead of leaking raw parser failures.
94+
- Exa: report malformed search API JSON with a provider-owned error instead of leaking raw parser failures.
95+
- Memory host SDK: report malformed remote JSON with caller-scoped errors for POST and batch file upload responses instead of leaking raw parser failures.
96+
- Media providers: report malformed operation-poll and audio-transcription JSON with provider-owned errors instead of leaking raw parser failures.
97+
- MiniMax, Gemini, Kimi, and Ollama web search: report malformed API JSON with provider-owned errors instead of leaking raw parser failures.
7998
- Twilio voice-call: report malformed successful API JSON responses with provider-owned errors instead of leaking raw parser failures.
8099
- Voice-call provider APIs: report malformed successful guarded JSON responses with provider-prefixed errors instead of leaking raw parser failures.
81100
- Realtime transcription: report malformed provider websocket JSON frames with owned parser errors instead of leaking raw `SyntaxError` objects.
@@ -90,11 +109,19 @@ Docs: https://docs.openclaw.ai
90109
- OpenAI embeddings: report malformed batch output JSONL with provider-owned errors instead of leaking raw parser failures.
91110
- Synology Chat: report malformed JSON webhook payloads with stable channel-owned parser errors.
92111
- Mattermost: report malformed interaction callback JSON with stable channel-owned parser errors.
112+
- Twilio voice-call: report malformed media stream WebSocket JSON with an owned parser error instead of logging raw parser failures.
113+
- Tlon/Urbit: report malformed SSE event JSON with an owned parser error instead of logging raw parser failures.
114+
- Signal: return a stable installer error when GitHub release metadata is malformed JSON.
115+
- ClawHub: report malformed successful marketplace JSON responses with owned errors instead of leaking raw parser failures.
116+
- Provider usage: report malformed successful usage JSON responses with stable provider errors instead of leaking raw parser failures.
117+
- Tlon/Urbit: report malformed scry response JSON with owned errors instead of leaking raw parser failures.
118+
- LM Studio: report malformed model list and model load JSON with owned errors instead of leaking raw parser failures.
93119
- Matrix: ignore malformed percent-encoding in optional location URI parameters instead of letting a bad `geo:` event abort inbound message handling.
94120
- Web search: auto-detect Brave through its legacy `tools.web.search.apiKey` compatibility fallback while keeping doctor migration to `plugins.entries.brave.config.webSearch.apiKey` as the canonical repair, so allowlisted isolated cron runs do not report `web_search` unavailable before migration. Fixes #81538. Thanks @atomicmonk.
95121
- Plugins: memoize repeated in-process plugin metadata snapshots and keep vanished managed-install residue from forcing full derived discovery, reducing gateway/status startup scans under large plugin sets. Fixes #81143 and #79806. (#81570) Thanks @Kaspre, @holgergruenhagen, @JanPlessow, and @mjamiv.
96122
- CLI/plugins: route lazy plugin command-registration chatter to stderr only during JSON-output command registration, keeping plugin-backed `--json` stdout parseable without changing parse-only or pass-through `--json` behavior. Fixes #81535. (#81536) Thanks @ScientificProgrammer and @vincentkoc.
97123
- Plugins: treat git plugin install refs as refs instead of checkout flags, so option-like selectors fail checkout instead of silently installing the default branch. Fixes #79898. (#79901) Thanks @afurm and @vincentkoc.
124+
- Doctor/memory: stop warning that no memory plugin is active when an enabled alternate memory plugin explicitly owns the memory slot, while preserving the warning for missing or disabled slot entries. Fixes #78540. (#78557) Thanks @carladams1299-lab and @vincentkoc.
98125
- Web: honor explicitly configured global `web_search` providers during provider ownership resolution while keeping sandboxed `web_fetch` limited to bundled providers.
99126
- Plugins/doctor: repair configured legacy npm declaration stubs by reinstalling their npm packages into the managed plugin root instead of loading workspace `node_modules`, and warn when discovery sees those stubs. Fixes #79632. Thanks @Dylanzhang1128 and @vincentkoc.
100127
- Channels: keep configured third-party channel plugins visible in `openclaw channels list` when their manifest declares `channels` but has not added `channelConfigs` metadata yet. Fixes #81334. (#81340) Thanks @AllynSheep and @vincentkoc.

extensions/brave/src/brave-web-search-provider.runtime.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
12
import type { SearchConfigRecord } from "openclaw/plugin-sdk/provider-web-search";
23
import {
34
buildSearchCacheKey,
@@ -231,7 +232,10 @@ async function runBraveLlmContextSearch(params: {
231232
);
232233
}
233234

234-
const data = (await response.json()) as BraveLlmContextResponse;
235+
const data = await readProviderJsonResponse<BraveLlmContextResponse>(
236+
response,
237+
"Brave LLM Context API error",
238+
);
235239
return { results: mapBraveLlmContextResults(data), sources: data.sources };
236240
},
237241
);
@@ -315,7 +319,10 @@ async function runBraveWebSearch(params: {
315319
);
316320
}
317321

318-
const data = (await response.json()) as BraveSearchResponse;
322+
const data = await readProviderJsonResponse<BraveSearchResponse>(
323+
response,
324+
"Brave Search API error",
325+
);
319326
const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
320327
return results.map((entry) => {
321328
const description = entry.description ?? "";

extensions/brave/src/brave-web-search-provider.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ describe("brave web search provider", () => {
243243
return {
244244
ok: true,
245245
json: async () => ({ web: { results: [] } }),
246-
} as Response;
246+
} as unknown as Response;
247247
});
248248
global.fetch = mockFetch as typeof global.fetch;
249249

@@ -293,6 +293,64 @@ describe("brave web search provider", () => {
293293
expect(requestUrl.pathname).toBe("/proxy/res/v1/llm/context");
294294
});
295295

296+
it("reports malformed Brave web search JSON as a provider error", async () => {
297+
vi.stubEnv("BRAVE_API_KEY", "");
298+
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
299+
return {
300+
ok: true,
301+
json: async () => {
302+
throw new SyntaxError("Unexpected token");
303+
},
304+
} as unknown as Response;
305+
});
306+
global.fetch = mockFetch as typeof global.fetch;
307+
308+
const provider = createBraveWebSearchProvider();
309+
const tool = provider.createTool({
310+
config: {},
311+
searchConfig: {
312+
apiKey: "brave-test-key",
313+
brave: { mode: "web" },
314+
},
315+
});
316+
if (!tool) {
317+
throw new Error("Expected tool definition");
318+
}
319+
320+
await expect(tool.execute({ query: "latest ai news" })).rejects.toThrow(
321+
"Brave Search API error: malformed JSON response",
322+
);
323+
});
324+
325+
it("reports malformed Brave llm-context JSON as a provider error", async () => {
326+
vi.stubEnv("BRAVE_API_KEY", "");
327+
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
328+
return {
329+
ok: true,
330+
json: async () => {
331+
throw new SyntaxError("Unexpected token");
332+
},
333+
} as unknown as Response;
334+
});
335+
global.fetch = mockFetch as typeof global.fetch;
336+
337+
const provider = createBraveWebSearchProvider();
338+
const tool = provider.createTool({
339+
config: {},
340+
searchConfig: {
341+
apiKey: "brave-test-key",
342+
brave: { mode: "llm-context" },
343+
},
344+
});
345+
if (!tool) {
346+
throw new Error("Expected tool definition");
347+
}
348+
349+
await expect(tool.execute({ query: "latest ai news" })).rejects.toThrow(
350+
"Brave LLM Context API error: malformed JSON response",
351+
);
352+
});
353+
296354
it("keeps Brave cache entries isolated by baseUrl", async () => {
297355
vi.stubEnv("BRAVE_API_KEY", "");
298356
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {

extensions/codex/src/app-server/rate-limits.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
22
import {
33
formatCodexUsageLimitErrorMessage,
44
resolveCodexUsageLimitResetAtMs,
5-
summarizeCodexRateLimits,
65
summarizeCodexAccountUsage,
6+
summarizeCodexRateLimits,
77
} from "./rate-limits.js";
88

99
describe("formatCodexUsageLimitErrorMessage", () => {
@@ -95,4 +95,57 @@ describe("summarizeCodexRateLimits", () => {
9595
),
9696
).toBe("Codex: primary 74% left ⏱3h · secondary 96% left ⏱7d");
9797
});
98+
99+
it("ignores empty named buckets instead of showing them as available limits", () => {
100+
const nowMs = 1_700_000_000_000;
101+
const payload = {
102+
rateLimitsByLimitId: {
103+
premium: {
104+
limitId: "premium",
105+
limitName: "premium",
106+
primary: null,
107+
secondary: null,
108+
credits: null,
109+
planType: "pro",
110+
rateLimitReachedType: null,
111+
},
112+
codex: {
113+
limitId: "codex",
114+
limitName: "Codex",
115+
primary: {
116+
usedPercent: 5,
117+
windowDurationMins: 300,
118+
resetsAt: Math.ceil(nowMs / 1000) + 3600,
119+
},
120+
secondary: null,
121+
credits: null,
122+
planType: "pro",
123+
rateLimitReachedType: null,
124+
},
125+
},
126+
};
127+
128+
expect(summarizeCodexRateLimits(payload, nowMs)).toContain("Codex: primary 95% left ⏱1h");
129+
expect(summarizeCodexRateLimits(payload, nowMs)).not.toContain("premium");
130+
expect(summarizeCodexAccountUsage(payload, nowMs)?.usageLine).toBe("short-term 5%");
131+
});
132+
133+
it("does not render a server-reported usage-limit block as available", () => {
134+
const payload = {
135+
rateLimits: {
136+
limitId: "codex",
137+
limitName: "Codex",
138+
primary: null,
139+
secondary: null,
140+
credits: null,
141+
rateLimitReachedType: "rate_limit_reached",
142+
},
143+
};
144+
145+
expect(summarizeCodexRateLimits(payload, 1_700_000_000_000)).toBe("Codex: rate limit reached");
146+
expect(summarizeCodexAccountUsage(payload, 1_700_000_000_000)).toMatchObject({
147+
blocked: true,
148+
blockingReason: "Codex usage limit is reached",
149+
});
150+
});
98151
});

extensions/codex/src/app-server/rate-limits.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,19 @@ export function summarizeCodexRateLimits(
7171
value: JsonValue | undefined,
7272
nowMs = Date.now(),
7373
): string | undefined {
74-
const snapshots = collectCodexRateLimitSnapshots(value);
74+
const snapshots = collectCodexRateLimitSnapshots(value).filter(snapshotHasDisplayableData);
7575
if (snapshots.length === 0) {
7676
return undefined;
7777
}
78-
return snapshots
78+
const summaries = snapshots
7979
.slice(0, 4)
8080
.map((snapshot) => summarizeRateLimitSnapshot(snapshot, nowMs))
81-
.join("; ");
81+
.filter((summary): summary is string => summary !== undefined);
82+
return summaries.length > 0 ? summaries.join("; ") : undefined;
83+
}
84+
85+
export function hasCodexRateLimitSnapshots(value: JsonValue | undefined): boolean {
86+
return collectCodexRateLimitSnapshots(value).length > 0;
8287
}
8388

8489
export function summarizeCodexAccountRateLimits(
@@ -113,7 +118,7 @@ export function summarizeCodexAccountUsage(
113118
value: JsonValue | undefined,
114119
nowMs = Date.now(),
115120
): CodexAccountUsageSummary | undefined {
116-
const snapshots = collectCodexRateLimitSnapshots(value);
121+
const snapshots = collectCodexRateLimitSnapshots(value).filter(snapshotHasDisplayableData);
117122
if (snapshots.length === 0) {
118123
return undefined;
119124
}
@@ -192,7 +197,7 @@ function selectBlockingRateLimitReset(
192197
return blockingSnapshot ? selectSnapshotBlockingReset(blockingSnapshot, nowMs) : undefined;
193198
}
194199

195-
function summarizeRateLimitSnapshot(snapshot: JsonObject, nowMs: number): string {
200+
function summarizeRateLimitSnapshot(snapshot: JsonObject, nowMs: number): string | undefined {
196201
const label = formatLimitLabel(snapshot);
197202
const windows = LIMIT_WINDOW_KEYS.flatMap((key) => {
198203
const window = readRateLimitWindow(snapshot, key);
@@ -201,7 +206,13 @@ function summarizeRateLimitSnapshot(snapshot: JsonObject, nowMs: number): string
201206
const reachedType =
202207
readString(snapshot, "rateLimitReachedType") ?? readString(snapshot, "rate_limit_reached_type");
203208
const suffix = reachedType ? ` (${formatReachedType(reachedType)})` : "";
204-
return `${label}: ${windows.join(" · ") || "available"}${suffix}`;
209+
if (windows.length > 0) {
210+
return `${label}: ${windows.join(" · ")}${suffix}`;
211+
}
212+
if (reachedType) {
213+
return `${label}: ${formatReachedType(reachedType)}`;
214+
}
215+
return undefined;
205216
}
206217

207218
function collectCodexRateLimitSnapshots(value: JsonValue | undefined): JsonObject[] {
@@ -314,6 +325,18 @@ function readRateLimitWindow(
314325
};
315326
}
316327

328+
function snapshotHasDisplayableData(snapshot: JsonObject): boolean {
329+
if (
330+
readString(snapshot, "rateLimitReachedType") ??
331+
readString(snapshot, "rate_limit_reached_type")
332+
) {
333+
return true;
334+
}
335+
return readWindowEntries(snapshot).some(
336+
(entry) => entry.window.usedPercent !== undefined || entry.window.resetsAtMs > 0,
337+
);
338+
}
339+
317340
function readOptionalNumberField(
318341
record: JsonObject,
319342
...keys: string[]

extensions/codex/src/command-formatters.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { CodexComputerUseStatus } from "./app-server/computer-use.js";
22
import type { CodexAppServerModelListResult } from "./app-server/models.js";
33
import { isJsonObject, type JsonObject, type JsonValue } from "./app-server/protocol.js";
44
import {
5+
hasCodexRateLimitSnapshots,
56
summarizeCodexAccountRateLimits,
67
summarizeCodexRateLimits,
78
} from "./app-server/rate-limits.js";
@@ -349,13 +350,21 @@ function summarizeArrayLike(value: JsonValue | undefined): string {
349350
}
350351

351352
function formatCodexRateLimitSummary(value: JsonValue | undefined): string {
352-
return formatCodexDisplayText(summarizeCodexRateLimits(value) ?? summarizeRateLimits(value));
353+
const summary = summarizeCodexRateLimits(value);
354+
if (summary) {
355+
return formatCodexDisplayText(summary);
356+
}
357+
return formatCodexDisplayText(
358+
hasCodexRateLimitSnapshots(value) ? "none returned" : summarizeRateLimits(value),
359+
);
353360
}
354361

355362
function formatCodexRateLimitDetails(value: JsonValue | undefined): string {
356363
const lines = summarizeCodexAccountRateLimits(value);
357364
if (!lines) {
358-
return formatCodexDisplayText(summarizeRateLimits(value));
365+
return formatCodexDisplayText(
366+
hasCodexRateLimitSnapshots(value) ? "none returned" : summarizeRateLimits(value),
367+
);
359368
}
360369
return lines.map(formatCodexDisplayText).join("\n");
361370
}

0 commit comments

Comments
 (0)