openclaw/scripts/lib/local-check-runtime.mts
Peter Steinberger d13f07b1c2
chore(deps): advance cooled dependencies and major upgrades (#146258)
* chore(deps): advance cooled dependencies and major upgrades

* test(logging): migrate failed-sink regression to tslog 5

* test: retain dependency upgrade coverage within lint limits

* fix(deps): preserve compiler launches, Matrix sync and chat metadata

Keep copied script harnesses independent of declaration modules and preserve
Windows executable prefixes after admission. Audit the Matrix sync guard for
42.3, align CI toolchain/cache pins, and refresh session facts after accepted
model-catalog invalidation without relying on picker timing.

* fix(ui): preserve scoped session reconciliation after catalog refresh
2026-09-12 13:28:12 -07:00

310 lines
10 KiB
TypeScript

// Applies resource policy for expensive local and CI check commands.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const GIB = 1024 ** 3;
const DEFAULT_LOCAL_GO_GC = "30";
const DEFAULT_LOCAL_GO_MAX_PROCS = 2;
const DEFAULT_LOCAL_GO_MEMORY_LIMIT = "3GiB";
const DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE = ".artifacts/tsgo-cache/root.tsbuildinfo";
const DEFAULT_FAST_LOCAL_CHECK_MIN_MEMORY_BYTES = 48 * GIB;
const DEFAULT_FAST_LOCAL_CHECK_MIN_CPUS = 12;
const CI_PARALLEL_MIN_CPUS = 8;
export const CI_PARALLEL_MIN_MEMORY_BYTES = 24 * GIB;
type Env = NodeJS.ProcessEnv;
type Resources = {
logicalCpuCount: number;
totalMemoryBytes: number;
};
type LocalCheckMode = "auto" | "full" | "throttled";
type RepoToolOptions = {
cwd?: string;
fileExists?: (candidate: string) => boolean;
resolveCommonDir?: (cwd: string) => string | null;
};
type NodeModulesLinkOptions = Pick<RepoToolOptions, "cwd" | "fileExists"> & {
symlink?: typeof fs.symlinkSync;
platform?: NodeJS.Platform;
};
/** Return whether local check safeguards are enabled for an environment. */
export function isLocalCheckEnabled(env: Env) {
const raw = env.OPENCLAW_LOCAL_CHECK?.trim().toLowerCase();
return raw !== "0" && raw !== "false";
}
function isCiLikeEnv(env: Env = process.env) {
return env.CI === "true" || env.GITHUB_ACTIONS === "true";
}
// Small CI runners share one constraint check for shard concurrency and Go memory policy.
export function isConstrainedCiCheckHost(hostResources: Resources) {
return !(
hostResources.totalMemoryBytes >= CI_PARALLEL_MIN_MEMORY_BYTES &&
hostResources.logicalCpuCount >= CI_PARALLEL_MIN_CPUS
);
}
/** Ensure local check runs opt into safeguard environment outside CI. */
export function resolveLocalCheckEnv(env: Env = process.env) {
if (isCiLikeEnv(env) || isLocalCheckEnabled(env)) {
return env;
}
return {
...env,
OPENCLAW_LOCAL_CHECK: "1",
};
}
/** Resolve a repo tool from this worktree or the primary checkout's installed toolchain. */
export function resolveRepoToolBinPath(
toolName: string,
{
cwd = process.cwd(),
fileExists = fs.existsSync,
resolveCommonDir = resolveGitCommonDir,
}: RepoToolOptions = {},
) {
if (toolName === "tsgo") {
// TypeScript 6 owns the in-process compiler API; CLI checks use the stable
// native compiler explicitly, independent of either package's tsc bin link.
const require = createRequire(import.meta.url);
const {
createDeclarationInputBoundary,
}: typeof import("./tsdown-declaration-boundary.mts") = require("./tsdown-declaration-boundary.mts");
const inputs = createDeclarationInputBoundary(cwd);
const fromCheckout = createRequire(path.join(inputs.root, "package.json"));
const nativeRoot = path.dirname(
inputs.assert(fromCheckout.resolve("typescript-native/package.json")),
);
const getExePath: { default: () => string } = require(
inputs.assert(path.join(nativeRoot, "lib/getExePath.js")),
);
const executable = getExePath.default();
// Windows launches need the extended-length prefix; normalize only for admission.
inputs.assert(fileURLToPath(pathToFileURL(executable)));
return executable;
}
const localPath = path.resolve(cwd, "node_modules", ".bin", toolName);
if (fileExists(localPath)) {
return localPath;
}
const commonDir = resolveCommonDir(cwd);
if (!commonDir || path.basename(commonDir) !== ".git") {
return localPath;
}
// Linked worktrees share the primary checkout's .git directory. Its parent
// owns the installed toolchain that dependency-less worktrees can reuse.
const primaryPath = path.join(path.dirname(commonDir), "node_modules", ".bin", toolName);
return fileExists(primaryPath) ? primaryPath : localPath;
}
/** Link explicitly provisioned dependencies for hydration or relocated declarations. */
export function ensureRepoNodeModulesLink(
modulesDir: string,
{
cwd = process.cwd(),
fileExists = fs.existsSync,
symlink = fs.symlinkSync,
platform = process.platform,
}: NodeModulesLinkOptions = {},
) {
const localNodeModules = path.resolve(cwd, "node_modules");
if (fileExists(localNodeModules)) {
return localNodeModules;
}
if (!fileExists(modulesDir)) {
return null;
}
try {
// Keep existing checkout dependencies locally owned; only absent modules
// reuse the selected installed toolchain, without reconciling dependencies.
symlink(modulesDir, localNodeModules, platform === "win32" ? "junction" : "dir");
} catch (error) {
// Another local runner may have installed the same stable link concurrently.
if (!fileExists(localNodeModules)) {
throw error;
}
}
return localNodeModules;
}
function hasFlag(args: string[], name: string) {
return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
}
function hasOxlintFormatArg(args: string[]) {
return args.some(
(arg) =>
arg === "--format" ||
arg.startsWith("--format=") ||
arg === "-f" ||
arg.startsWith("-f=") ||
(arg.startsWith("-f") && arg.length > 2),
);
}
/** Apply the shared memory and scheduler limits for Go-backed check helpers. */
function applyThrottledGoRuntimeEnv(env: Env, hostResources: Resources) {
if (!env.GOMAXPROCS) {
env.GOMAXPROCS = String(
Math.min(DEFAULT_LOCAL_GO_MAX_PROCS, Math.max(1, hostResources.logicalCpuCount)),
);
}
if (!env.GOGC) {
env.GOGC = DEFAULT_LOCAL_GO_GC;
}
if (!env.GOMEMLIMIT) {
env.GOMEMLIMIT = DEFAULT_LOCAL_GO_MEMORY_LIMIT;
}
}
/** Apply local tsgo defaults for declaration skipping, caching, throttling, and profiling. */
export function applyLocalTsgoPolicy(args: string[], env: Env, hostResources: Resources) {
const nextEnv = { ...env };
const nextArgs = [...args];
const defaultProjectRun = nextArgs.length === 0;
if (!hasFlag(nextArgs, "--declaration") && !nextArgs.includes("-d")) {
insertBeforeSeparator(nextArgs, "--declaration", "false");
}
const localCheckEnabled = isLocalCheckEnabled(nextEnv);
if (localCheckEnabled && defaultProjectRun) {
insertBeforeSeparator(nextArgs, "--incremental");
insertBeforeSeparator(
nextArgs,
"--tsBuildInfoFile",
nextEnv.OPENCLAW_TSGO_BUILD_INFO_FILE ?? DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE,
);
}
const resolvedHostResources = resolveHostResources(hostResources);
if (
shouldThrottleLocalChecks(nextEnv, resolvedHostResources, "auto") ||
(isCiLikeEnv(nextEnv) && isConstrainedCiCheckHost(resolvedHostResources))
) {
insertBeforeSeparator(nextArgs, "--singleThreaded");
insertBeforeSeparator(nextArgs, "--checkers", "1");
applyThrottledGoRuntimeEnv(nextEnv, resolvedHostResources);
}
if (localCheckEnabled && nextEnv.OPENCLAW_TSGO_PPROF_DIR && !hasFlag(nextArgs, "--pprofDir")) {
insertBeforeSeparator(nextArgs, "--pprofDir", nextEnv.OPENCLAW_TSGO_PPROF_DIR);
}
return { env: nextEnv, args: nextArgs };
}
/** Apply oxlint defaults for type-aware checking and throttled worker settings. */
export function applyLocalOxlintPolicy(args: string[], env: Env, hostResources: Resources) {
const nextEnv = { ...env };
const nextArgs = [...args];
insertBeforeSeparator(nextArgs, "--type-aware");
insertBeforeSeparator(nextArgs, "--tsconfig", "config/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 (nextEnv.GITHUB_ACTIONS === "true" && !hasOxlintFormatArg(nextArgs)) {
insertBeforeSeparator(nextArgs, "--format", "stylish");
}
if (
shouldThrottleLocalChecks(nextEnv, hostResources) ||
(isCiLikeEnv(nextEnv) && isConstrainedCiCheckHost(hostResources))
) {
if (!hasFlag(nextArgs, "--threads")) {
insertBeforeSeparator(nextArgs, "--threads=1");
}
// Oxlint's thread flag does not govern the Go tsgolint helper.
applyThrottledGoRuntimeEnv(nextEnv, hostResources);
}
return { env: nextEnv, args: nextArgs };
}
function shouldThrottleLocalChecks(
env: Env,
hostResources: Resources | undefined,
defaultMode: LocalCheckMode = "throttled",
) {
if (!isLocalCheckEnabled(env)) {
return false;
}
const mode = readLocalCheckMode(env, defaultMode);
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
);
}
function resolveGitCommonDir(cwd: string) {
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 insertBeforeSeparator(args: string[], ...items: [string, ...string[]]) {
if (hasFlag(args, items[0])) {
return;
}
const separatorIndex = args.indexOf("--");
const insertIndex = separatorIndex === -1 ? args.length : separatorIndex;
args.splice(insertIndex, 0, ...items);
}
function readLocalCheckMode(env: Env, defaultMode: LocalCheckMode) {
const raw = env.OPENCLAW_LOCAL_CHECK_MODE?.trim().toLowerCase();
if (raw === "throttled" || raw === "low-memory") {
return "throttled";
}
if (raw === "full" || raw === "fast") {
return "full";
}
return defaultMode;
}
function resolveHostResources(hostResources: Resources | undefined) {
if (hostResources) {
return hostResources;
}
return {
totalMemoryBytes: os.totalmem(),
logicalCpuCount:
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
};
}