mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-12 22:30:51 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
395
openclaw/scripts/lib/local-heavy-check-runtime.mjs
Normal file
395
openclaw/scripts/lib/local-heavy-check-runtime.mjs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const GIB = 1024 ** 3;
|
||||
const DEFAULT_LOCAL_GO_GC = "30";
|
||||
const DEFAULT_LOCAL_GO_MEMORY_LIMIT = "3GiB";
|
||||
const DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE = ".artifacts/tsgo-cache/root.tsbuildinfo";
|
||||
const DEFAULT_LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const DEFAULT_LOCK_POLL_MS = 500;
|
||||
const DEFAULT_LOCK_PROGRESS_MS = 15 * 1000;
|
||||
const DEFAULT_STALE_LOCK_MS = 30 * 1000;
|
||||
const DEFAULT_FAST_LOCAL_CHECK_MIN_MEMORY_BYTES = 48 * GIB;
|
||||
const DEFAULT_FAST_LOCAL_CHECK_MIN_CPUS = 12;
|
||||
const SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
||||
|
||||
export function isLocalCheckEnabled(env) {
|
||||
const raw = env.OPENCLAW_LOCAL_CHECK?.trim().toLowerCase();
|
||||
return raw !== "0" && raw !== "false";
|
||||
}
|
||||
|
||||
export function hasFlag(args, name) {
|
||||
return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
||||
}
|
||||
|
||||
export function applyLocalTsgoPolicy(args, env, hostResources) {
|
||||
const nextEnv = { ...env };
|
||||
const nextArgs = [...args];
|
||||
const defaultProjectRun = nextArgs.length === 0;
|
||||
|
||||
if (!hasFlag(nextArgs, "--declaration") && !nextArgs.includes("-d")) {
|
||||
insertBeforeSeparator(nextArgs, "--declaration", "false");
|
||||
}
|
||||
|
||||
if (!isLocalCheckEnabled(nextEnv)) {
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
if (defaultProjectRun) {
|
||||
insertBeforeSeparator(nextArgs, "--incremental");
|
||||
insertBeforeSeparator(
|
||||
nextArgs,
|
||||
"--tsBuildInfoFile",
|
||||
nextEnv.OPENCLAW_TSGO_BUILD_INFO_FILE ?? DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE,
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldThrottleLocalHeavyChecks(nextEnv, hostResources)) {
|
||||
insertBeforeSeparator(nextArgs, "--singleThreaded");
|
||||
insertBeforeSeparator(nextArgs, "--checkers", "1");
|
||||
|
||||
if (!nextEnv.GOGC) {
|
||||
nextEnv.GOGC = DEFAULT_LOCAL_GO_GC;
|
||||
}
|
||||
if (!nextEnv.GOMEMLIMIT) {
|
||||
nextEnv.GOMEMLIMIT = DEFAULT_LOCAL_GO_MEMORY_LIMIT;
|
||||
}
|
||||
}
|
||||
if (nextEnv.OPENCLAW_TSGO_PPROF_DIR && !hasFlag(nextArgs, "--pprofDir")) {
|
||||
insertBeforeSeparator(nextArgs, "--pprofDir", nextEnv.OPENCLAW_TSGO_PPROF_DIR);
|
||||
}
|
||||
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
export function applyLocalOxlintPolicy(args, env, hostResources) {
|
||||
const nextEnv = { ...env };
|
||||
const nextArgs = [...args];
|
||||
|
||||
insertBeforeSeparator(nextArgs, "--type-aware");
|
||||
insertBeforeSeparator(nextArgs, "--tsconfig", "tsconfig.oxlint.json");
|
||||
if (
|
||||
!hasFlag(nextArgs, "--report-unused-disable-directives") &&
|
||||
!hasFlag(nextArgs, "--report-unused-disable-directives-severity")
|
||||
) {
|
||||
insertBeforeSeparator(nextArgs, "--report-unused-disable-directives-severity", "error");
|
||||
}
|
||||
|
||||
if (shouldThrottleLocalHeavyChecks(nextEnv, hostResources)) {
|
||||
insertBeforeSeparator(nextArgs, "--threads=1");
|
||||
}
|
||||
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
export function shouldAcquireLocalHeavyCheckLockForOxlint(
|
||||
args,
|
||||
{ cwd = process.cwd(), env = process.env } = {},
|
||||
) {
|
||||
if (env.OPENCLAW_OXLINT_FORCE_LOCK === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
args.some(
|
||||
(arg) =>
|
||||
arg === "--help" ||
|
||||
arg === "-h" ||
|
||||
arg === "--version" ||
|
||||
arg === "-V" ||
|
||||
arg === "--rules" ||
|
||||
arg === "--print-config" ||
|
||||
arg === "--init",
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const separatorIndex = args.indexOf("--");
|
||||
const candidateArgs = (() => {
|
||||
if (separatorIndex !== -1) {
|
||||
return args.slice(separatorIndex + 1);
|
||||
}
|
||||
const firstFlagIndex = args.findIndex((arg) => arg.startsWith("-"));
|
||||
return firstFlagIndex === -1 ? args : args.slice(0, firstFlagIndex);
|
||||
})();
|
||||
const explicitTargets = candidateArgs.filter((arg) => arg.length > 0 && !arg.startsWith("-"));
|
||||
if (explicitTargets.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !explicitTargets.every((target) => {
|
||||
try {
|
||||
return fs.statSync(path.resolve(cwd, target)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldAcquireLocalHeavyCheckLockForTsgo(args, env = process.env) {
|
||||
if (env.OPENCLAW_TSGO_FORCE_LOCK === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !args.some(
|
||||
(arg) =>
|
||||
arg === "--help" ||
|
||||
arg === "-h" ||
|
||||
arg === "--version" ||
|
||||
arg === "-v" ||
|
||||
arg === "--init" ||
|
||||
arg === "--showConfig",
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldThrottleLocalHeavyChecks(env, hostResources) {
|
||||
if (!isLocalCheckEnabled(env)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mode = readLocalCheckMode(env);
|
||||
if (mode === "throttled") {
|
||||
return true;
|
||||
}
|
||||
if (mode === "full") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedHostResources = resolveHostResources(hostResources);
|
||||
return (
|
||||
resolvedHostResources.totalMemoryBytes < DEFAULT_FAST_LOCAL_CHECK_MIN_MEMORY_BYTES ||
|
||||
resolvedHostResources.logicalCpuCount < DEFAULT_FAST_LOCAL_CHECK_MIN_CPUS
|
||||
);
|
||||
}
|
||||
|
||||
export function acquireLocalHeavyCheckLockSync(params) {
|
||||
const env = params.env ?? process.env;
|
||||
|
||||
if (!isLocalCheckEnabled(env)) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const commonDir = resolveGitCommonDir(params.cwd);
|
||||
const locksDir = path.join(commonDir, "openclaw-local-checks");
|
||||
const lockDir = path.join(locksDir, `${params.lockName ?? "heavy-check"}.lock`);
|
||||
const ownerPath = path.join(lockDir, "owner.json");
|
||||
const timeoutMs = readPositiveInt(
|
||||
env.OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS,
|
||||
DEFAULT_LOCK_TIMEOUT_MS,
|
||||
);
|
||||
const pollMs = readPositiveInt(env.OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS, DEFAULT_LOCK_POLL_MS);
|
||||
const progressMs = readPositiveInt(
|
||||
env.OPENCLAW_HEAVY_CHECK_LOCK_PROGRESS_MS,
|
||||
DEFAULT_LOCK_PROGRESS_MS,
|
||||
);
|
||||
const staleLockMs = readPositiveInt(
|
||||
env.OPENCLAW_HEAVY_CHECK_STALE_LOCK_MS,
|
||||
DEFAULT_STALE_LOCK_MS,
|
||||
);
|
||||
const startedAt = Date.now();
|
||||
let waitingLogged = false;
|
||||
let lastProgressAt = 0;
|
||||
|
||||
fs.mkdirSync(locksDir, { recursive: true });
|
||||
if (!params.lockName) {
|
||||
cleanupLegacyLockDirs(locksDir, staleLockMs);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
try {
|
||||
fs.mkdirSync(lockDir);
|
||||
writeOwnerFile(ownerPath, {
|
||||
pid: process.pid,
|
||||
tool: params.toolName,
|
||||
cwd: params.cwd,
|
||||
hostname: os.hostname(),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
return () => {
|
||||
fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
};
|
||||
} catch (error) {
|
||||
if (!isAlreadyExistsError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const owner = readOwnerFile(ownerPath);
|
||||
if (shouldReclaimLock({ owner, lockDir, staleLockMs })) {
|
||||
fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
if (elapsedMs >= timeoutMs) {
|
||||
const ownerLabel = describeOwner(owner);
|
||||
throw new Error(
|
||||
`[${params.toolName}] timed out waiting for the local heavy-check lock at ${lockDir}${
|
||||
ownerLabel ? ` (${ownerLabel})` : ""
|
||||
}. If no local heavy checks are still running, remove the stale lock and retry.`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
||||
if (!waitingLogged) {
|
||||
const ownerLabel = describeOwner(owner);
|
||||
console.error(
|
||||
`[${params.toolName}] queued behind the local heavy-check lock${
|
||||
ownerLabel ? ` held by ${ownerLabel}` : ""
|
||||
}...`,
|
||||
);
|
||||
waitingLogged = true;
|
||||
lastProgressAt = Date.now();
|
||||
} else if (Date.now() - lastProgressAt >= progressMs) {
|
||||
const ownerLabel = describeOwner(owner);
|
||||
console.error(
|
||||
`[${params.toolName}] still waiting ${formatElapsedMs(elapsedMs)} for the local heavy-check lock${
|
||||
ownerLabel ? ` held by ${ownerLabel}` : ""
|
||||
}...`,
|
||||
);
|
||||
lastProgressAt = Date.now();
|
||||
}
|
||||
|
||||
sleepSync(pollMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveGitCommonDir(cwd) {
|
||||
const result = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
|
||||
if (result.status === 0) {
|
||||
const raw = result.stdout.trim();
|
||||
if (raw.length > 0) {
|
||||
return path.resolve(cwd, raw);
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(cwd, ".git");
|
||||
}
|
||||
|
||||
function cleanupLegacyLockDirs(locksDir, staleLockMs) {
|
||||
for (const legacyLockName of ["test"]) {
|
||||
const legacyLockDir = path.join(locksDir, `${legacyLockName}.lock`);
|
||||
if (!fs.existsSync(legacyLockDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const owner = readOwnerFile(path.join(legacyLockDir, "owner.json"));
|
||||
if (shouldReclaimLock({ owner, lockDir: legacyLockDir, staleLockMs })) {
|
||||
fs.rmSync(legacyLockDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertBeforeSeparator(args, ...items) {
|
||||
if (items.length > 0 && hasFlag(args, items[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const separatorIndex = args.indexOf("--");
|
||||
const insertIndex = separatorIndex === -1 ? args.length : separatorIndex;
|
||||
args.splice(insertIndex, 0, ...items);
|
||||
}
|
||||
|
||||
function readLocalCheckMode(env) {
|
||||
const raw = env.OPENCLAW_LOCAL_CHECK_MODE?.trim().toLowerCase();
|
||||
if (raw === "throttled" || raw === "low-memory") {
|
||||
return "throttled";
|
||||
}
|
||||
if (raw === "full" || raw === "fast") {
|
||||
return "full";
|
||||
}
|
||||
// Keep local heavy checks conservative by default. Developers can still opt
|
||||
// into full-speed runs explicitly with OPENCLAW_LOCAL_CHECK_MODE=full.
|
||||
return "throttled";
|
||||
}
|
||||
|
||||
function resolveHostResources(hostResources) {
|
||||
if (hostResources) {
|
||||
return hostResources;
|
||||
}
|
||||
|
||||
return {
|
||||
totalMemoryBytes: os.totalmem(),
|
||||
logicalCpuCount:
|
||||
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
|
||||
};
|
||||
}
|
||||
|
||||
function readPositiveInt(rawValue, fallback) {
|
||||
const parsed = Number.parseInt(rawValue ?? "", 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function writeOwnerFile(ownerPath, owner) {
|
||||
fs.writeFileSync(ownerPath, `${JSON.stringify(owner, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function readOwnerFile(ownerPath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(ownerPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isAlreadyExistsError(error) {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
||||
}
|
||||
|
||||
function shouldReclaimLock({ owner, lockDir, staleLockMs }) {
|
||||
if (owner && typeof owner.pid === "number") {
|
||||
return !isProcessAlive(owner.pid);
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(lockDir);
|
||||
return Date.now() - stats.mtimeMs >= staleLockMs;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function isProcessAlive(pid) {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
|
||||
}
|
||||
}
|
||||
|
||||
function describeOwner(owner) {
|
||||
if (!owner || typeof owner !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const tool = typeof owner.tool === "string" ? owner.tool : "unknown-tool";
|
||||
const pid = typeof owner.pid === "number" ? `pid ${owner.pid}` : "unknown pid";
|
||||
const cwd = typeof owner.cwd === "string" ? owner.cwd : "unknown cwd";
|
||||
return `${tool}, ${pid}, cwd ${cwd}`;
|
||||
}
|
||||
|
||||
function formatElapsedMs(elapsedMs) {
|
||||
if (elapsedMs < 1000) {
|
||||
return `${elapsedMs}ms`;
|
||||
}
|
||||
const seconds = elapsedMs / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${seconds.toFixed(seconds >= 10 ? 0 : 1)}s`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainderSeconds = Math.round(seconds % 60);
|
||||
return `${minutes}m ${remainderSeconds}s`;
|
||||
}
|
||||
|
||||
function sleepSync(ms) {
|
||||
Atomics.wait(SLEEP_BUFFER, 0, 0, ms);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue