Skip to content

Commit 19f997e

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents e66608c + 8fe2c1b commit 19f997e

21 files changed

Lines changed: 2427 additions & 37 deletions
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"startupJsGzipBytes": 345049,
3-
"reason": "streamed-markdown highlighting changes (#127749, #127754) consumed the ratchet",
4-
"updatedAt": "2026-08-22"
2+
"startupJsGzipBytes": 340388,
3+
"reason": "rebase onto current main; startup growth landed on main since the boot-group ratchet",
4+
"updatedAt": "2026-08-24"
55
}

extensions/anthropic/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"description": "OpenClaw Anthropic provider, Claude CLI, and native session catalog plugin",
66
"type": "module",
77
"dependencies": {
8-
"@anthropic-ai/claude-agent-sdk": "0.3.219"
8+
"@anthropic-ai/claude-agent-sdk": "0.3.232"
99
},
1010
"devDependencies": {
1111
"@openclaw/plugin-sdk": "workspace:*"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2004,6 +2004,7 @@
20042004
"tui:pty:test:watch:all": "node --import tsx scripts/dev/tui-pty-test-watch.ts --mode all",
20052005
"tui:pty:test:watch:fake": "node --import tsx scripts/dev/tui-pty-test-watch.ts --mode fake",
20062006
"tui:pty:test:watch:local": "node --import tsx scripts/dev/tui-pty-test-watch.ts --mode local",
2007+
"ui:boot-manifest:gen": "node --import tsx scripts/control-ui-boot-manifest.mts",
20072008
"ui:build": "node scripts/ui.js build",
20082009
"ui:dev": "node scripts/ui.js dev",
20092010
"ui:i18n:baseline": "node --import tsx scripts/control-ui-i18n-verify.ts baseline",

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/check-control-ui-performance.mts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ const controlUiPerformanceBudgets = {
3333
// sidebar zone styling; headroom over the ~36.5 KiB post-diet baseline.
3434
startupCssGzipBytes: 45 * KIB,
3535
largestJsGzipBytes: 215 * KIB,
36-
largestCssGzipBytes: 45 * KIB,
36+
// Startup CSS stays at 45 KiB; the boot-group consolidation (2026-08,
37+
// control-ui-boot chunking) merges boot-path component CSS into one file
38+
// that lands just above it, trading ~1 KiB of ceiling for ~95 fewer boot
39+
// requests on HTTP/1.1 gateways.
40+
largestCssGzipBytes: 47 * KIB,
3741
} satisfies Record<string, number>;
3842
export const CONTROL_UI_PERFORMANCE_BUDGETS = Object.freeze(controlUiPerformanceBudgets);
3943

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env -S node --import tsx
2+
// Regenerates ui/config/control-ui-boot-modules.json: the measured module set
3+
// the default Control UI boot flow loads lazily. Boots the built dist bundle
4+
// against the mocked Gateway, records every JS chunk fetched through chat
5+
// readiness, and unions their sourcemap sources into canonical manifest keys.
6+
// Requires a current `pnpm ui:build` output in dist/control-ui.
7+
import fs from "node:fs";
8+
import http from "node:http";
9+
import path from "node:path";
10+
import { fileURLToPath } from "node:url";
11+
import { chromium } from "playwright";
12+
import { controlUiBootManifestKey } from "../ui/config/control-ui-chunking.ts";
13+
import { installMockGateway } from "../ui/src/test-helpers/control-ui-e2e.ts";
14+
15+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
16+
const distDir = path.join(repoRoot, "dist", "control-ui");
17+
const manifestPath = path.join(repoRoot, "ui", "config", "control-ui-boot-modules.json");
18+
const SETTLE_MS = 3_000;
19+
const READY_TIMEOUT_MS = 60_000;
20+
21+
const mime: Record<string, string> = {
22+
".html": "text/html",
23+
".js": "text/javascript",
24+
".css": "text/css",
25+
".json": "application/json",
26+
".svg": "image/svg+xml",
27+
".map": "application/json",
28+
".webmanifest": "application/manifest+json",
29+
};
30+
31+
function serveDist(): Promise<{ baseUrl: string; close: () => void }> {
32+
const server = http.createServer((req, res) => {
33+
const urlPath = new URL(req.url ?? "/", "http://localhost").pathname;
34+
if (urlPath === "/control-ui-config.json") {
35+
res.setHeader("Content-Type", "application/json");
36+
res.end(JSON.stringify({ basePath: "/", assistantName: "", assistantAvatar: "" }));
37+
return;
38+
}
39+
let filePath = path.join(distDir, urlPath === "/" ? "index.html" : urlPath.slice(1));
40+
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
41+
filePath = path.join(distDir, "index.html");
42+
}
43+
res.setHeader("Content-Type", mime[path.extname(filePath)] ?? "application/octet-stream");
44+
res.end(fs.readFileSync(filePath));
45+
});
46+
return new Promise((resolve) => {
47+
server.listen(0, "127.0.0.1", () => {
48+
const address = server.address();
49+
if (address === null || typeof address !== "object") {
50+
throw new Error("Control UI boot manifest server has no port");
51+
}
52+
resolve({
53+
baseUrl: `http://127.0.0.1:${address.port}`,
54+
close: () => server.close(),
55+
});
56+
});
57+
});
58+
}
59+
60+
function readDistBuildId(): string {
61+
const swSource = fs.readFileSync(path.join(distDir, "sw.js"), "utf8");
62+
const buildId = /EMBEDDED_CACHE_VERSION = "([^"]+)"/.exec(swSource)?.[1];
63+
if (!buildId) {
64+
throw new Error("Control UI boot manifest cannot read the dist build id from sw.js");
65+
}
66+
return buildId;
67+
}
68+
69+
async function collectBootChunkPaths(baseUrl: string): Promise<Set<string>> {
70+
const browser = await chromium.launch();
71+
try {
72+
const page = await browser.newPage();
73+
const chunkPaths = new Set<string>();
74+
page.on("request", (request) => {
75+
const { pathname } = new URL(request.url());
76+
if (pathname.startsWith("/assets/") && pathname.endsWith(".js")) {
77+
chunkPaths.add(pathname);
78+
}
79+
});
80+
await installMockGateway(page, { serverBuildId: readDistBuildId() });
81+
await page.goto(`${baseUrl}/chat`, { waitUntil: "commit" });
82+
// Chat readiness proves the boot flow completed instead of stalling on an
83+
// error surface; a manifest captured from a broken boot would be garbage.
84+
await page
85+
.locator(".agent-chat__composer-combobox textarea")
86+
.waitFor({ timeout: READY_TIMEOUT_MS });
87+
await page.waitForTimeout(SETTLE_MS);
88+
return chunkPaths;
89+
} finally {
90+
await browser.close();
91+
}
92+
}
93+
94+
function manifestKeysForChunks(chunkPaths: Iterable<string>): string[] {
95+
const keys = new Set<string>();
96+
for (const chunkPath of chunkPaths) {
97+
const mapPath = path.join(distDir, `${chunkPath}.map`);
98+
if (!fs.existsSync(mapPath)) {
99+
// Facade chunks for dynamic entries can omit maps; their modules are
100+
// covered by the chunks that carry the actual code.
101+
continue;
102+
}
103+
const map = JSON.parse(fs.readFileSync(mapPath, "utf8")) as { sources?: string[] };
104+
for (const source of map.sources ?? []) {
105+
keys.add(controlUiBootManifestKey(path.resolve(path.join(distDir, "assets"), source)));
106+
}
107+
}
108+
return [...keys].toSorted();
109+
}
110+
111+
async function main(): Promise<void> {
112+
if (!fs.existsSync(path.join(distDir, "index.html"))) {
113+
throw new Error(`No Control UI build at ${distDir}; run \`pnpm ui:build\` first`);
114+
}
115+
const server = await serveDist();
116+
try {
117+
const chunkPaths = await collectBootChunkPaths(server.baseUrl);
118+
const keys = manifestKeysForChunks(chunkPaths);
119+
if (keys.length < 100) {
120+
throw new Error(`Boot capture looks truncated: only ${keys.length} modules recorded`);
121+
}
122+
fs.writeFileSync(manifestPath, `${JSON.stringify(keys, null, 1)}\n`);
123+
console.log(
124+
`control-ui-boot-manifest: ${chunkPaths.size} boot chunks -> ${keys.length} modules -> ${path.relative(repoRoot, manifestPath)}`,
125+
);
126+
} finally {
127+
server.close();
128+
}
129+
}
130+
131+
main().catch((error: unknown) => {
132+
console.error(error);
133+
console.error("[control-ui-boot-manifest] FAILED (exit 1)");
134+
process.exit(1);
135+
});

src/audit/audit-event-writer.test.ts

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { tableExists } from "../state/openclaw-state-db-schema-helpers.js";
77
import {
88
closeOpenClawStateDatabaseForTest,
99
openOpenClawStateDatabase,
10+
registerOpenClawStateDatabaseLifecycleListener,
1011
} from "../state/openclaw-state-db.js";
1112
import { listAuditEvents, recordAuditEvent } from "./audit-event-store.js";
1213
import type { AuditEventInput } from "./audit-event-types.js";
@@ -26,6 +27,32 @@ import {
2627
} from "./execution-identity-context.js";
2728
import type { TrustedMessageAuditEvent } from "./message-audit-events.js";
2829

30+
function observeNonblockingSqliteTransactions(
31+
database: DatabaseSync,
32+
observed: number[],
33+
): () => void {
34+
const originalExecDescriptor = Object.getOwnPropertyDescriptor(database, "exec");
35+
const originalExec = database.exec.bind(database);
36+
database.exec = (sql: string) => {
37+
if (sql === "BEGIN IMMEDIATE") {
38+
const busyTimeout = readSqliteBusyTimeout(database);
39+
observed.push(busyTimeout);
40+
if (busyTimeout !== 0) {
41+
throw new Error(`audit writer attempted a blocking SQLite transaction (${busyTimeout} ms)`);
42+
}
43+
}
44+
return originalExec(sql);
45+
};
46+
return () => {
47+
if (originalExecDescriptor) {
48+
Object.defineProperty(database, "exec", originalExecDescriptor);
49+
return;
50+
}
51+
const ownDatabaseMethod: { exec?: DatabaseSync["exec"] } = database;
52+
delete ownDatabaseMethod.exec;
53+
};
54+
}
55+
2956
function defineObjectPrototypeProperties(descriptors: PropertyDescriptorMap): void {
3057
// oxlint-disable-next-line no-extend-native -- Exercise hostile prototype pollution across the real clone boundary.
3158
Object.defineProperties(Object.prototype, descriptors);
@@ -262,20 +289,40 @@ describe("audit event writer", () => {
262289
const contender = new DatabaseSync(path);
263290
contender.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
264291
const errors: string[] = [];
265-
const probeStartedAt = performance.now();
292+
const observedBusyTimeouts: number[] = [];
293+
let openedBusyTimeout: number | undefined;
294+
let restoreExec: (() => void) | undefined;
295+
const clearDatabaseListener = registerOpenClawStateDatabaseLifecycleListener((event) => {
296+
if (event.kind !== "opened" || event.database.path !== path) {
297+
return;
298+
}
299+
openedBusyTimeout = readSqliteBusyTimeout(event.database.db);
300+
restoreExec = observeNonblockingSqliteTransactions(event.database.db, observedBusyTimeouts);
301+
});
266302
const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) });
267303

268304
try {
269-
const eventLoopDelay = await new Promise<number>((resolve) => {
270-
setTimeout(() => resolve(performance.now() - probeStartedAt), 25);
271-
});
272-
expect(eventLoopDelay).toBeLessThan(250);
273305
await writer.ready;
306+
await new Promise<void>((resolve) => {
307+
setImmediate(resolve);
308+
});
309+
expect(contender.isTransaction).toBe(true);
310+
expect(openedBusyTimeout).toBe(0);
311+
expect(observedBusyTimeouts).not.toHaveLength(0);
312+
expect(observedBusyTimeouts.every((busyTimeout) => busyTimeout === 0)).toBe(true);
274313
expect(writer.record({ ...input(), sourceId: "cold-owner", runId: "cold-owner" })).toBe(true);
275314
} finally {
276-
contender.exec("ROLLBACK");
277-
contender.close();
278-
await writer.stop();
315+
try {
316+
contender.exec("ROLLBACK");
317+
contender.close();
318+
} finally {
319+
try {
320+
await writer.stop();
321+
} finally {
322+
restoreExec?.();
323+
clearDatabaseListener();
324+
}
325+
}
279326
}
280327

281328
expect(errors).toEqual([]);
@@ -342,6 +389,8 @@ describe("audit event writer", () => {
342389
db.exec("DELETE FROM audit_identity_keys;");
343390
const contender = new DatabaseSync(path);
344391
contender.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
392+
const observedBusyTimeouts: number[] = [];
393+
const restoreExec = observeNonblockingSqliteTransactions(db, observedBusyTimeouts);
345394
const clearSink = configureExecutionIdentityAdmissionSink(writer.recordExecutionIdentity);
346395
const admittedAt = Date.now();
347396

@@ -380,11 +429,12 @@ describe("audit event writer", () => {
380429
accepted: true,
381430
});
382431
expect(performance.now() - startedAt).toBeLessThan(250);
383-
const eventLoopProbeStartedAt = performance.now();
384-
const eventLoopDelay = await new Promise<number>((resolve) => {
385-
setTimeout(() => resolve(performance.now() - eventLoopProbeStartedAt), 25);
432+
await new Promise<void>((resolve) => {
433+
setImmediate(resolve);
386434
});
387-
expect(eventLoopDelay).toBeLessThan(250);
435+
expect(contender.isTransaction).toBe(true);
436+
expect(observedBusyTimeouts).not.toHaveLength(0);
437+
expect(observedBusyTimeouts.every((busyTimeout) => busyTimeout === 0)).toBe(true);
388438
expect(readSqliteBusyTimeout(db)).toBe(5_000);
389439
expect(
390440
writer.recordExecutionIdentity({
@@ -411,7 +461,11 @@ describe("audit event writer", () => {
411461
contender.close();
412462
} finally {
413463
clearSink();
414-
await writer.stop();
464+
try {
465+
await writer.stop();
466+
} finally {
467+
restoreExec();
468+
}
415469
}
416470
}
417471

ui/AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ This directory owns Control UI-specific guidance that should not live in the rep
4242
- Method-advertisement checks (`isGatewayMethodAdvertised`) remain only as feature gates for config/plugin-dependent surfaces, never as version compat.
4343
- The handshake rejects gateway-served same-origin skew. The admission-exempt paths (`pnpm ui:dev`, custom `gateway.controlUi.root`, cross-origin/connection-settings dialing) are unsupported for version mismatch without enforcement: they carry no compat code and fail visibly at the first missing method, by design. Tightening admission to reject them at connect is a server-side product change owned separately.
4444

45+
## Build Chunking
46+
47+
- `ui/config/control-ui-boot-modules.json` is a generated manifest of the modules the default boot flow loads lazily; the `control-ui-boot` group in `ui/config/control-ui-chunking.ts` merges them into a few chunks so boot avoids ~140 HTTP/1.1 requests. Regenerate with `pnpm ui:boot-manifest:gen` after `pnpm ui:build` when boot-path surfaces change materially; stale entries degrade to extra chunks, never breakage. Do not hand-edit the manifest.
48+
4549
## Live Verification
4650

4751
- The Gateway serves the prebuilt bundle from `dist/control-ui`; editing `ui/src` changes nothing live until `pnpm ui:build`. Confirm the served `/assets/index-*.js` hash changed before trusting a live result.

0 commit comments

Comments
 (0)