Skip to content

Commit 3122775

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 4ce54ed + b5fa2ed commit 3122775

25 files changed

Lines changed: 1986 additions & 220 deletions

CHANGELOG.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,28 @@ Docs: https://docs.openclaw.ai
44

55
## Unreleased
66

7-
- fix(ui): replace marked.js with markdown-it to fix ReDoS UI freeze (#46707) thanks @zhangfnf
7+
- UI/chat: replace marked.js with markdown-it so maliciously crafted markdown can no longer freeze the Control UI via ReDoS. (#46707) Thanks @zhangfnf.
88

99
### Changes
1010

1111
- Telegram/forum topics: surface human topic names in agent context, prompt metadata, and plugin hook metadata by learning names from Telegram forum service messages. (#65973) Thanks @ptahdunbar.
1212

1313
### Fixes
1414

15-
- fix(heartbeat): force owner downgrade for untrusted hook:wake system events [AI-assisted]. (#66031) Thanks @pgondhi987.
16-
- fix(browser): enforce SSRF policy on snapshot, screenshot, and tab routes [AI]. (#66040) Thanks @pgondhi987.
17-
- fix(msteams): enforce sender allowlist checks on SSO signin invokes [AI]. (#66033) Thanks @pgondhi987.
18-
- fix(config): redact sourceConfig and runtimeConfig alias fields in redactConfigSnapshot [AI]. (#66030) Thanks @pgondhi987.
19-
- Agents/context engines: run opt-in turn maintenance as idle-aware background work so the next foreground turn no longer waits on proactive maintenance. (#65233) thanks @100yenadmin
20-
21-
- Plugins/status: report the registered context-engine IDs in `plugins inspect` instead of the owning plugin ID, so non-matching engine IDs and multi-engine plugins are classified correctly. (#58766) thanks @zhuisDEV
15+
- Auto-reply/send policy: keep `sendPolicy: "deny"` from blocking inbound message processing, so the agent still runs its turn while all outbound delivery is suppressed for observer-style setups. (#65461, #53328) Thanks @omarshahine.
16+
- BlueBubbles: lazy-refresh the Private API server-info cache on send when reply threading or message effects are requested but status is unknown, so sends no longer silently degrade to plain messages when the 10-minute cache expires. (#65447, #43764) Thanks @omarshahine.
17+
- Heartbeat/security: force owner downgrade for untrusted `hook:wake` system events [AI-assisted]. (#66031) Thanks @pgondhi987.
18+
- Browser/security: enforce SSRF policy on snapshot, screenshot, and tab routes [AI]. (#66040) Thanks @pgondhi987.
19+
- Microsoft Teams/security: enforce sender allowlist checks on SSO signin invokes [AI]. (#66033) Thanks @pgondhi987.
20+
- Config/security: redact `sourceConfig` and `runtimeConfig` alias fields in `redactConfigSnapshot` [AI]. (#66030) Thanks @pgondhi987.
21+
- Agents/context engines: run opt-in turn maintenance as idle-aware background work so the next foreground turn no longer waits on proactive maintenance. (#65233) Thanks @100yenadmin.
22+
- Plugins/status: report the registered context-engine IDs in `plugins inspect` instead of the owning plugin ID, so non-matching engine IDs and multi-engine plugins are classified correctly. (#58766) Thanks @zhuisDEV.
2223
- Context engines: reject resolved plugin engines whose reported `info.id` does not match their registered slot id, so malformed engines fail fast before id-based runtime branches can misbehave. (#63222) Thanks @fuller-stack-dev.
2324
- WhatsApp: patch installed Baileys media encryption writes during OpenClaw postinstall so the default npm/install.sh delivery path waits for encrypted media files to finish flushing before readback, avoiding transient `ENOENT` crashes on image sends. (#65896) Thanks @frankekn.
2425
- Gateway/update: unify service entrypoint resolution around the canonical bundled gateway entrypoint so update, reinstall, and doctor repair stop drifting between stale `dist/entry.js` and current `dist/index.js` paths. (#65984) Thanks @mbelinky.
2526
- Heartbeat/Telegram topics: keep isolated heartbeat replies on the bound forum topic when `target=last`, instead of dropping them into the group root chat. (#66035) Thanks @mbelinky.
2627
- Browser/CDP: let managed local Chrome readiness, status probes, and managed loopback CDP control bypass browser SSRF policy for their own loopback control plane, so OpenClaw no longer misclassifies a healthy child browser as "not reachable after start". (#65695, #66043) Thanks @mbelinky.
27-
- Gateway/sessions: stop heartbeat, cron-event, and exec-event turns from overwriting shared-session routing and origin metadata, preventing synthetic `heartbeat` targets from poisoning later cron or user delivery. (#63733, #35300)
28+
- Gateway/sessions: stop heartbeat, cron-event, and exec-event turns from overwriting shared-session routing and origin metadata, preventing synthetic `heartbeat` targets from poisoning later cron or user delivery. (#66073, #63733, #35300) Thanks @mbelinky.
2829
- Browser/CDP: let local attach-only `manual-cdp` profiles reuse the local loopback CDP control plane under strict default policy and remote-class probe timeouts, so tabs/snapshot stop falsely reporting a live local browser session as not running. (#65611, #66080) Thanks @mbelinky.
2930
- Cron/scheduler: stop inventing short retries when cron next-run calculation returns no valid future slot, and keep a maintenance wake armed so enabled unscheduled jobs recover without entering a refire loop. (#66019, #66083) Thanks @mbelinky.
3031
- Cron/scheduler: preserve the active error-backoff floor when maintenance repair recomputes a missing cron next-run, so recurring errored jobs do not resume early after a transient next-run resolution failure. (#66019, #66083, #66113) Thanks @mbelinky.
@@ -33,11 +34,15 @@ Docs: https://docs.openclaw.ai
3334
- Dreaming/memory-core: require a live queued Dreaming cron event before the heartbeat hook runs the sweep, so managed Dreaming no longer replays on later heartbeats after the scheduled run was already consumed. (#66139) Thanks @mbelinky.
3435
- Control UI/Dreaming: stop Imported Insights and Memory Palace from calling optional `memory-wiki` gateway methods when the plugin is off, and refresh config before wiki reloads so the Dreaming tab stops showing misleading unknown-method failures. (#66140) Thanks @mbelinky.
3536
- Agents/tools: only mark streamed unknown-tool retries as counted when a streamed message actually classifies an unavailable tool, and keep incomplete streamed tool names from resetting the retry streak before the final assistant message arrives. (#66145) Thanks @dutifulbob.
36-
- Memory/active-memory: move recalled memory onto the hidden untrusted prompt-prefix path instead of system prompt injection, label the visible Active Memory status line fields, and include the resolved recall provider/model in gateway debug logs so trace/debug output matches what the model actually saw.
37+
- Memory/active-memory: move recalled memory onto the hidden untrusted prompt-prefix path instead of system prompt injection, label the visible Active Memory status line fields, and include the resolved recall provider/model in gateway debug logs so trace/debug output matches what the model actually saw. (#66144) Thanks @Takhoffman.
3738
- Memory/QMD: stop treating legacy lowercase `memory.md` as a second default root collection, so QMD recall no longer searches phantom `memory-alt-*` collections and builtin/QMD root-memory fallback stays aligned. (#66141) Thanks @mbelinky.
38-
- Agents/OpenAI: map `minimal` thinking to OpenAI's supported `low` reasoning effort for GPT-5.4 requests, so embedded runs stop failing request validation.
39+
- Agents/OpenAI: map `minimal` thinking to OpenAI's supported `low` reasoning effort for GPT-5.4 requests, so embedded runs stop failing request validation. Thanks @steipete.
3940
- Voice-call/media-stream: resolve the source IP from trusted forwarding headers for per-IP pending-connection limits when `webhookSecurity.trustForwardingHeaders` and `trustedProxyIPs` are configured, and reserve `maxConnections` capacity for in-flight WebSocket upgrades so concurrent handshakes can no longer momentarily exceed the operator-set cap. (#66027) Thanks @eleqtrizit.
4041
- Feishu/allowlist: canonicalize allowlist entries by explicit `user`/`chat` kind, strip repeated `feishu:`/`lark:` provider prefixes, and stop folding opaque Feishu IDs to lowercase, so allowlist matching no longer crosses user/chat namespaces or widens to case-insensitive ID matches the operator did not intend. (#66021) Thanks @eleqtrizit.
42+
- TTS/reply media: persist OpenClaw temp voice outputs into managed outbound media and allow them through reply-media normalization, so voice-note replies stop silently dropping. (#63511) Thanks @jetd1.
43+
- Agents/OpenAI: recover embedded GPT-style runs when reasoning-only or empty turns need bounded continuation, with replay-safe retry gating and incomplete-turn fallback when no visible answer arrives. (#66167) thanks @jalehman
44+
- Outbound/relay-status: suppress internal relay-status placeholder payloads (`No channel reply.`, `Replied in-thread.`, `Replied in #...`, wiki-update status variants ending in `No channel reply.`) before channel delivery so internal housekeeping text does not leak to users.
45+
- Slack/doctor: add a dedicated doctor-contract sidecar so config warmup paths such as `openclaw cron` no longer fall back to Slack's broader contract surface, which could trigger Slack-related config-read crashes on affected setups. (#63192) Thanks @shhtheonlyperson.
4146

4247
## 2026.4.12
4348

@@ -118,6 +123,7 @@ Docs: https://docs.openclaw.ai
118123
- Agents/CLI: keep unrelated config, session, transcript, and MCP bootstrap runtime off common `openclaw agent` cold paths so provider selection and agent startup stop stalling on heavyweight imports. Thanks @vincentkoc.
119124
- Setup/config/install: stop setup, config dry-runs, and daemon install from eagerly booting auth-profile and plugin repair runtime when those paths are not needed, so onboarding and local service setup avoid long cold-start stalls. Thanks @vincentkoc.
120125
- Cron/direct delivery: slim isolated-agent delivery cold paths so direct channel delivery and related cron execution spend less time loading unrelated auth, plugin, and channel runtime. Thanks @vincentkoc.
126+
- Channels/replay dedupe: standardize replay claims, retryable-failure release, and post-success commit behavior across Telegram, Discord, Slack, Mattermost, WhatsApp, Matrix, LINE, Feishu, Zalo, Nextcloud Talk, TLON, Nostr, Voice Call, and shared plugin interactive callbacks so duplicate deliveries stay reply-once after success but retry cleanly after pre-delivery failures. Thanks @vincentkoc.
121127

122128
## 2026.4.11
123129

extensions/qa-lab/src/mock-openai-server.test.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,65 @@ import { resolveProviderVariant, startQaMockOpenAiServer } from "./mock-openai-s
44
const cleanups: Array<() => Promise<void>> = [];
55
const QA_IMAGE_PNG_BASE64 =
66
"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAT0lEQVR42u3RQQkAMAzAwPg33Wnos+wgBo40dboAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANYADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAC+Azy47PDiI4pA2wAAAABJRU5ErkJggg==";
7+
const QA_REASONING_ONLY_RECOVERY_PROMPT =
8+
"Reasoning-only continuation QA check: read QA_KICKOFF_TASK.md, then answer with exactly REASONING-RECOVERED-OK.";
9+
const QA_REASONING_ONLY_SIDE_EFFECT_PROMPT =
10+
"Reasoning-only after write safety check: write reasoning-only-side-effect.txt, then answer with exactly SIDE-EFFECT-GUARD-OK.";
11+
const QA_EMPTY_RESPONSE_RECOVERY_PROMPT =
12+
"Empty response continuation QA check: read QA_KICKOFF_TASK.md, then answer with exactly EMPTY-RECOVERED-OK.";
13+
const QA_EMPTY_RESPONSE_EXHAUSTION_PROMPT =
14+
"Empty response exhaustion QA check: read QA_KICKOFF_TASK.md, then answer with exactly EMPTY-EXHAUSTED-OK.";
15+
const QA_REASONING_ONLY_RETRY_INSTRUCTION =
16+
"The previous assistant turn recorded reasoning but did not produce a user-visible answer. Continue from that partial turn and produce the visible answer now. Do not restate the reasoning or restart from scratch.";
17+
const QA_EMPTY_RESPONSE_RETRY_INSTRUCTION =
18+
"The previous attempt did not produce a user-visible answer. Continue from the current state and produce the visible answer now. Do not restart from scratch.";
719

820
afterEach(async () => {
921
while (cleanups.length > 0) {
1022
await cleanups.pop()?.();
1123
}
1224
});
1325

26+
async function startMockServer() {
27+
const server = await startQaMockOpenAiServer({
28+
host: "127.0.0.1",
29+
port: 0,
30+
});
31+
cleanups.push(async () => {
32+
await server.stop();
33+
});
34+
return server;
35+
}
36+
37+
async function postResponses(server: { baseUrl: string }, body: unknown) {
38+
return fetch(`${server.baseUrl}/v1/responses`, {
39+
method: "POST",
40+
headers: {
41+
"content-type": "application/json",
42+
},
43+
body: JSON.stringify(body),
44+
});
45+
}
46+
47+
async function expectResponsesText(server: { baseUrl: string }, body: unknown) {
48+
const response = await postResponses(server, body);
49+
expect(response.status).toBe(200);
50+
return response.text();
51+
}
52+
53+
async function expectResponsesJson<T>(server: { baseUrl: string }, body: unknown) {
54+
const response = await postResponses(server, body);
55+
expect(response.status).toBe(200);
56+
return (await response.json()) as T;
57+
}
58+
59+
function makeUserInput(text: string) {
60+
return {
61+
role: "user" as const,
62+
content: [{ type: "input_text" as const, text }],
63+
};
64+
}
65+
1466
describe("qa mock openai server", () => {
1567
it("serves health and streamed responses", async () => {
1668
const server = await startQaMockOpenAiServer({
@@ -1750,6 +1802,204 @@ describe("qa mock openai server", () => {
17501802
const debug = (await debugResponse.json()) as { model: string };
17511803
expect(debug.model).toBe("claude-opus-4-6");
17521804
});
1805+
1806+
it("scripts a reasoning-only recovery sequence after a replay-safe read", async () => {
1807+
const server = await startMockServer();
1808+
1809+
const toolPlan = await expectResponsesText(server, {
1810+
stream: true,
1811+
model: "gpt-5.4",
1812+
input: [makeUserInput(QA_REASONING_ONLY_RECOVERY_PROMPT)],
1813+
});
1814+
expect(toolPlan).toContain('"name":"read"');
1815+
expect(toolPlan).toContain("QA_KICKOFF_TASK.md");
1816+
1817+
expect(
1818+
await expectResponsesJson<{
1819+
output?: Array<{ type?: string; id?: string; summary?: Array<{ text?: string }> }>;
1820+
}>(server, {
1821+
stream: false,
1822+
model: "gpt-5.4",
1823+
input: [
1824+
makeUserInput(QA_REASONING_ONLY_RECOVERY_PROMPT),
1825+
{
1826+
type: "function_call_output",
1827+
output: "QA mission: Understand this OpenClaw repo from source + docs before acting.",
1828+
},
1829+
],
1830+
}),
1831+
).toMatchObject({
1832+
output: [
1833+
{
1834+
type: "reasoning",
1835+
id: "rs_mock_reasoning_recovery",
1836+
summary: [{ text: expect.stringContaining("Need visible answer") }],
1837+
},
1838+
],
1839+
});
1840+
1841+
expect(
1842+
await expectResponsesJson<{
1843+
output?: Array<{ content?: Array<{ text?: string }> }>;
1844+
}>(server, {
1845+
stream: false,
1846+
model: "gpt-5.4",
1847+
input: [
1848+
makeUserInput(QA_REASONING_ONLY_RECOVERY_PROMPT),
1849+
makeUserInput(QA_REASONING_ONLY_RETRY_INSTRUCTION),
1850+
{
1851+
type: "function_call_output",
1852+
output: "QA mission: Understand this OpenClaw repo from source + docs before acting.",
1853+
},
1854+
],
1855+
}),
1856+
).toMatchObject({
1857+
output: [
1858+
{
1859+
content: [{ text: "REASONING-RECOVERED-OK" }],
1860+
},
1861+
],
1862+
});
1863+
1864+
const requests = await fetch(`${server.baseUrl}/debug/requests`);
1865+
expect(requests.status).toBe(200);
1866+
expect(await requests.json()).toMatchObject([
1867+
{ plannedToolName: "read" },
1868+
{ allInputText: expect.stringContaining(QA_REASONING_ONLY_RECOVERY_PROMPT) },
1869+
{ allInputText: expect.stringContaining(QA_REASONING_ONLY_RETRY_INSTRUCTION) },
1870+
]);
1871+
});
1872+
1873+
it("keeps the reasoning-only side-effect path ready for no-auto-retry QA coverage", async () => {
1874+
const server = await startMockServer();
1875+
1876+
const toolPlan = await expectResponsesText(server, {
1877+
stream: true,
1878+
model: "gpt-5.4",
1879+
input: [makeUserInput(QA_REASONING_ONLY_SIDE_EFFECT_PROMPT)],
1880+
});
1881+
expect(toolPlan).toContain('"name":"write"');
1882+
expect(toolPlan).toContain("reasoning-only-side-effect.txt");
1883+
1884+
expect(
1885+
await expectResponsesJson<{
1886+
output?: Array<{ type?: string; id?: string }>;
1887+
}>(server, {
1888+
stream: false,
1889+
model: "gpt-5.4",
1890+
input: [
1891+
makeUserInput(QA_REASONING_ONLY_SIDE_EFFECT_PROMPT),
1892+
{
1893+
type: "function_call_output",
1894+
output: "Successfully wrote 28 bytes to reasoning-only-side-effect.txt.",
1895+
},
1896+
],
1897+
}),
1898+
).toMatchObject({
1899+
output: [{ type: "reasoning", id: "rs_mock_reasoning_side_effect" }],
1900+
});
1901+
1902+
const requests = await fetch(`${server.baseUrl}/debug/requests`);
1903+
expect(requests.status).toBe(200);
1904+
expect((await requests.json()) as Array<{ allInputText?: string }>).toHaveLength(2);
1905+
});
1906+
1907+
it("scripts an empty-response recovery sequence after a replay-safe read", async () => {
1908+
const server = await startMockServer();
1909+
1910+
const toolPlan = await expectResponsesText(server, {
1911+
stream: true,
1912+
model: "gpt-5.4",
1913+
input: [makeUserInput(QA_EMPTY_RESPONSE_RECOVERY_PROMPT)],
1914+
});
1915+
expect(toolPlan).toContain('"name":"read"');
1916+
1917+
expect(
1918+
await expectResponsesJson<{
1919+
output?: Array<{ content?: Array<{ type?: string; text?: string }> }>;
1920+
}>(server, {
1921+
stream: false,
1922+
model: "gpt-5.4",
1923+
input: [
1924+
makeUserInput(QA_EMPTY_RESPONSE_RECOVERY_PROMPT),
1925+
{
1926+
type: "function_call_output",
1927+
output: "QA mission: Understand this OpenClaw repo from source + docs before acting.",
1928+
},
1929+
],
1930+
}),
1931+
).toMatchObject({
1932+
output: [
1933+
{
1934+
content: [{ type: "output_text", text: "" }],
1935+
},
1936+
],
1937+
});
1938+
1939+
expect(
1940+
await expectResponsesJson<{
1941+
output?: Array<{ content?: Array<{ text?: string }> }>;
1942+
}>(server, {
1943+
stream: false,
1944+
model: "gpt-5.4",
1945+
input: [
1946+
makeUserInput(QA_EMPTY_RESPONSE_RECOVERY_PROMPT),
1947+
makeUserInput(QA_EMPTY_RESPONSE_RETRY_INSTRUCTION),
1948+
{
1949+
type: "function_call_output",
1950+
output: "QA mission: Understand this OpenClaw repo from source + docs before acting.",
1951+
},
1952+
],
1953+
}),
1954+
).toMatchObject({
1955+
output: [
1956+
{
1957+
content: [{ text: "EMPTY-RECOVERED-OK" }],
1958+
},
1959+
],
1960+
});
1961+
});
1962+
1963+
it("can keep emitting empty GPT turns when the single retry budget should exhaust", async () => {
1964+
const server = await startMockServer();
1965+
1966+
await expectResponsesText(server, {
1967+
stream: true,
1968+
model: "gpt-5.4",
1969+
input: [makeUserInput(QA_EMPTY_RESPONSE_EXHAUSTION_PROMPT)],
1970+
});
1971+
1972+
const firstEmpty = await expectResponsesJson<{
1973+
output?: Array<{ content?: Array<{ text?: string }> }>;
1974+
}>(server, {
1975+
stream: false,
1976+
model: "gpt-5.4",
1977+
input: [
1978+
makeUserInput(QA_EMPTY_RESPONSE_EXHAUSTION_PROMPT),
1979+
{
1980+
type: "function_call_output",
1981+
output: "QA mission: Understand this OpenClaw repo from source + docs before acting.",
1982+
},
1983+
],
1984+
});
1985+
expect(firstEmpty.output?.[0]?.content?.[0]?.text).toBe("");
1986+
1987+
const secondEmpty = await expectResponsesJson<{
1988+
output?: Array<{ content?: Array<{ text?: string }> }>;
1989+
}>(server, {
1990+
stream: false,
1991+
model: "gpt-5.4",
1992+
input: [
1993+
makeUserInput(QA_EMPTY_RESPONSE_EXHAUSTION_PROMPT),
1994+
makeUserInput(QA_EMPTY_RESPONSE_RETRY_INSTRUCTION),
1995+
{
1996+
type: "function_call_output",
1997+
output: "QA mission: Understand this OpenClaw repo from source + docs before acting.",
1998+
},
1999+
],
2000+
});
2001+
expect(secondEmpty.output?.[0]?.content?.[0]?.text).toBe("");
2002+
});
17532003
});
17542004

17552005
describe("resolveProviderVariant", () => {

0 commit comments

Comments
 (0)