fix(gateway): prevent control-plane polling stalls (#124891)

* fix(gateway): avoid repeated control-plane scans

* fix(tooling): allow concurrent worktree validation

* fix(ci): refresh protocol and runner inputs

* perf(ui): defer hidden session refreshes

* fix(ui): resolve session refresh lint failure

* fix(update): preserve pre-cache update channel

* fix(update): normalize cached update channel

* fix(gateway): lifecycle-cache update install identity

* fix(ui): preserve manual history retry after layout scroll

* test(codex): repair side-question tool schema fixture
This commit is contained in:
Peter Steinberger
2026-08-16 20:58:27 -07:00
committed by GitHub
parent d24cb13320
commit bf9b25ab7e
46 changed files with 1082 additions and 1904 deletions
-8
View File
@@ -11,17 +11,9 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
- For changed-file verification, prefer `scripts/check-changed.mjs` and keep lane classification in `scripts/changed-lanes.mjs`. Use `node scripts/check-changed.mjs --dry-run [--staged|-- <files...>]` to inspect the plan before running anything expensive. Do not copy path-scope rules into new hooks or ad hoc CI snippets.
- For one/few lint files, prefer direct `node scripts/run-oxlint.mjs --tsconfig <matching config> <files...>` over sharded `pnpm lint`; `check-changed.mjs` owns this targeting for core, extension, and script diffs.
## Local Heavy-Check Lock
- Respect the local heavy-check lock behavior in `scripts/lib/local-heavy-check-runtime.mts`.
- Do not bypass that lock for real heavy commands just to make a local loop look faster.
- Metadata-only or explicitly narrow commands may skip the lock when the existing helper logic says that is safe.
- If you change the lock heuristics, add or update the narrow tests under `test/scripts/`.
## PR Prepare Gates
- `scripts/pr` serializes review, prepare, and merge operations per PR across linked worktrees; `scripts/pr gc` skips active or indeterminate locks. Its subcommand classification table is the canonical wrapper trust boundary: a mismatched local wrapper may run only a classified `advisory` subcommand with `--dev-wrapper` or `OPENCLAW_PR_DEV_WRAPPER=1`; classified `landing` subcommands always require canonical/origin-main wrapper code. A worktree whose wrapper differs from origin/main (stale base or wrapper-editing branch) loudly substitutes the canonical checkout's wrapper when that checkout is clean and byte-identical to fetched `refs/remotes/origin/main`; it refuses only when no anchor-matching wrapper is available. A successful command return is the trusted synchronous-completion contract: every PR-state-mutating child must be joined before returning, and such work must never daemonize or explicitly escape both the operation group and lock-notification FD. Release on clean exit requires the leader's completion marker; an escaped descendant that merely holds the notify pipe then produces a loud warned release instead of retention (#124583), while all failure shapes still retain. A failed command auto-releases only while its explicit pre-side-effect validation marker remains active; failures after mutation/tool launch, interruptions, and controller loss stay locked because detached children cannot be disproved. After verifying no child tools remain, use the reported exact-OID `scripts/pr lock-recover` command. Never bypass or delete these refs manually.
- `scripts/pr prepare-gates` holds the heavy-check lock for its whole local gate block (`scripts/pr-gates-lock.mts`), so concurrent gate runs across `.worktrees` queue as units instead of dying on child lock timeouts or vitest no-output watchdog kills.
- `OPENCLAW_PR_GATES_REMOTE=testbox` runs the full-suite `pnpm test` gate on a Blacksmith Testbox through `scripts/crabbox-wrapper.mjs` (same delegation as `check:changed`); `pnpm build`/`pnpm check` stay local. The `tbx_` lease id and Actions run URL land in `.local/gates.env` (`REMOTE_GATES_*`) and `.local/prep.md`. Use it for reviewed trusted code when a loaded host makes the local 88-shard run stall-kill; contributor/fork code stays on secretless CI or sanitized AWS unless a maintainer explicitly approves credentialed execution.
## Generated Outputs
+20 -35
View File
@@ -30,11 +30,7 @@ import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-fa
import { printTimingSummary } from "./lib/check-timing-summary.mts";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { runWithFailedTrailer } from "./lib/failed-trailer.mts";
import {
acquireLocalHeavyCheckLockSync,
resolveLocalHeavyCheckEnv,
withLocalHeavyCheckLockHeld,
} from "./lib/local-heavy-check-runtime.mts";
import { resolveLocalCheckEnv } from "./lib/local-check-runtime.mts";
import { runManagedCommand } from "./lib/managed-child-process.mts";
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mts";
import { createSparseTsgoSkipEnv } from "./lib/tsgo-sparse-guard.mts";
@@ -156,8 +152,8 @@ if (!isDirectRun()) {
await ensureChangedCheckRuntimeDependencies(["package.json"]);
}
export function createChangedCheckChildEnv(baseEnv: NodeJS.ProcessEnv = process.env) {
return withLocalHeavyCheckLockHeld(resolveLocalHeavyCheckEnv(baseEnv));
function createChangedCheckChildEnv(baseEnv: NodeJS.ProcessEnv = process.env) {
return resolveLocalCheckEnv(baseEnv);
}
function hasAndroidVersionSyncPath(paths: string[]) {
@@ -1047,41 +1043,30 @@ async function runChangedCheck(result: ChangedLaneResult, options: ChangedCheckR
return 0;
}
await ensureChangedCheckRuntimeDependencies(result.paths);
const baseEnv = resolveLocalHeavyCheckEnv(options.env ?? process.env);
const baseEnv = resolveLocalCheckEnv(options.env ?? process.env);
const childEnv = createChangedCheckChildEnv(baseEnv);
const plan = createChangedCheckPlan(result, {
...options,
env: childEnv,
});
const releaseLock = options.dryRun
? () => {}
: acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env: baseEnv,
toolName: "check:changed",
});
try {
printPlan(result, plan, options);
printPlan(result, plan, options);
if (options.dryRun) {
return 0;
}
const timings: ChangedCheckTiming[] = [];
for (const command of plan.commands) {
const status = await runPlanCommand(command, timings);
if (status !== 0) {
printSummary(timings, options);
return status;
}
}
printSummary(timings, options);
if (options.dryRun) {
return 0;
} finally {
releaseLock();
}
const timings: ChangedCheckTiming[] = [];
for (const command of plan.commands) {
const status = await runPlanCommand(command, timings);
if (status !== 0) {
printSummary(timings, options);
return status;
}
}
printSummary(timings, options);
return 0;
}
function sameArgs(left: string[], right: string[]) {
@@ -1132,7 +1117,7 @@ export function createPnpmManagedCommand<T extends ChangedCheckCommand>(
command: T,
env: NodeJS.ProcessEnv = process.env,
) {
const commandEnv = command.env ?? resolveLocalHeavyCheckEnv(env);
const commandEnv = command.env ?? resolveLocalCheckEnv(env);
if (isOpenEndedTruthyValue(commandEnv.CI) || isOpenEndedTruthyValue(commandEnv.GITHUB_ACTIONS)) {
const shimmedEnv = prependCorepackPnpmShim(commandEnv);
return {
@@ -1195,7 +1180,7 @@ async function runCommand(
status = await runManagedCommand({
bin: command.bin,
args: command.args,
env: command.env ?? resolveLocalHeavyCheckEnv(),
env: command.env ?? resolveLocalCheckEnv(),
});
} catch (error) {
console.error(error);
+1 -1
View File
@@ -1,6 +1,6 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mts";
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
import { runManagedCommand } from "./lib/managed-child-process.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
+1 -1
View File
@@ -2,7 +2,7 @@
// Enforces core tsgo project boundaries and sparse-checkout safety.
import { spawnSync } from "node:child_process";
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mts";
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
import { createManagedCommandInvocation } from "./lib/managed-child-process.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
+2 -17
View File
@@ -19,10 +19,6 @@ import type { Readable } from "node:stream";
import { StringDecoder } from "node:string_decoder";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import {
acquireLocalHeavyCheckLockSync,
withLocalHeavyCheckLockHeld,
} from "./lib/local-heavy-check-runtime.mts";
// Two concurrent plans halve the serial tail of packed jobs. Children run with
// inner test-projects parallelism 1 so a job never exceeds two Vitest runs;
@@ -146,9 +142,7 @@ export function buildChildEnv(
delete childEnv.OPENCLAW_VITEST_INCLUDE_FILE;
}
}
// This wrapper owns the shared lock, so every nested heavy-check wrapper must
// inherit that ownership rather than queueing behind its own parent.
return withLocalHeavyCheckLockHeld(childEnv);
return childEnv;
}
export function pruneFsModuleCache(root: string, maxBytes = FS_MODULE_CACHE_MAX_BYTES) {
@@ -392,14 +386,5 @@ if (isDirectRunUrl(process.argv[1], import.meta.url)) {
// Bins holding spawn/signal-timing suites are marked planConcurrency 1 by
// the planner; overlapping them with a sibling Vitest run causes flakes.
const planConcurrency = Number(process.env.OPENCLAW_NODE_TEST_PLAN_CONCURRENCY) || undefined;
const releaseLock = acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env: process.env,
toolName: "test",
});
try {
process.exitCode = await runShardPlans(plans, { concurrency: planConcurrency });
} finally {
releaseLock();
}
process.exitCode = await runShardPlans(plans, { concurrency: planConcurrency });
}
+1 -1
View File
@@ -6,7 +6,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mts";
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
import { outputTail } from "./lib/output-tail.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
@@ -0,0 +1,86 @@
import fs from "node:fs";
import path from "node:path";
const LOCK_POLL_MS = 500;
const LOCK_TIMEOUT_MS = 10 * 60 * 1000;
const OWNER_WRITE_GRACE_MS = 30 * 1000;
const SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
type LockOwner = { pid?: unknown };
function isProcessAlive(pid: unknown) {
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch (error) {
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
}
}
function shouldReclaimLock(lockDir: string, ownerPath: string) {
try {
const owner = JSON.parse(fs.readFileSync(ownerPath, "utf8")) as LockOwner;
return !isProcessAlive(owner.pid);
} catch {
try {
return Date.now() - fs.statSync(lockDir).mtimeMs >= OWNER_WRITE_GRACE_MS;
} catch {
return true;
}
}
}
export function acquireExtensionPackageBoundaryArtifactLockSync(rootDir: string) {
// The generated declarations live in this checkout. Keeping ownership beside
// them prevents same-worktree writers without coupling independent worktrees.
const lockDir = path.join(rootDir, "dist", ".extension-package-boundary-artifacts.lock");
const ownerPath = path.join(lockDir, "owner.json");
const startedAt = Date.now();
let reportedWait = false;
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
for (;;) {
try {
fs.mkdirSync(lockDir);
fs.writeFileSync(
ownerPath,
`${JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2)}\n`,
"utf8",
);
let released = false;
const release = () => {
if (released) {
return;
}
released = true;
fs.rmSync(lockDir, { force: true, recursive: true });
};
process.once("exit", release);
return () => {
process.off("exit", release);
release();
};
} catch (error) {
if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) {
throw error;
}
if (shouldReclaimLock(lockDir, ownerPath)) {
fs.rmSync(lockDir, { force: true, recursive: true });
continue;
}
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
throw new Error(`timed out waiting for plugin package-boundary artifacts in ${rootDir}`, {
cause: error,
});
}
if (!reportedWait) {
console.error("[plugin package-boundary artifacts] waiting for the current writer...");
reportedWait = true;
}
Atomics.wait(SLEEP_BUFFER, 0, 0, LOCK_POLL_MS);
}
}
}
+284
View File
@@ -0,0 +1,284 @@
// Applies local resource policy for expensive check commands.
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_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;
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;
};
/** 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";
}
/** 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 = {},
) {
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 a dependency-less worktree to the primary checkout toolchain selected above. */
export function ensureRepoToolNodeModulesLink(
toolPath: string,
{
cwd = process.cwd(),
fileExists = fs.existsSync,
resolveCommonDir = resolveGitCommonDir,
symlink = fs.symlinkSync,
platform = process.platform,
}: RepoToolOptions & {
symlink?: typeof fs.symlinkSync;
platform?: NodeJS.Platform;
} = {},
) {
const localNodeModules = path.resolve(cwd, "node_modules");
if (fileExists(localNodeModules)) {
return localNodeModules;
}
const commonDir = resolveCommonDir(cwd);
if (!commonDir || path.basename(commonDir) !== ".git") {
return null;
}
const primaryNodeModules = path.join(path.dirname(commonDir), "node_modules");
const toolNodeModules = path.dirname(path.dirname(path.resolve(toolPath)));
if (toolNodeModules !== path.resolve(primaryNodeModules) || !fileExists(primaryNodeModules)) {
return null;
}
try {
// Match run-vitest.mjs's hydrated-toolchain behavior: keep one stable link
// so compilers can resolve imports from worktree source paths.
symlink(primaryNodeModules, 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");
}
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,
);
}
const resolvedHostResources = resolveHostResources(hostResources);
if (shouldThrottleLocalChecks(nextEnv, resolvedHostResources, "auto")) {
insertBeforeSeparator(nextArgs, "--singleThreaded");
insertBeforeSeparator(nextArgs, "--checkers", "1");
applyThrottledGoRuntimeEnv(nextEnv, resolvedHostResources);
}
if (nextEnv.OPENCLAW_TSGO_PPROF_DIR && !hasFlag(nextArgs, "--pprofDir")) {
insertBeforeSeparator(nextArgs, "--pprofDir", nextEnv.OPENCLAW_TSGO_PPROF_DIR);
}
return { env: nextEnv, args: nextArgs };
}
/** Apply local 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)) {
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,
};
}
-595
View File
@@ -1,595 +0,0 @@
// Applies local resource policy and process locks for expensive check commands.
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_MAX_PROCS = 2;
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));
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 LockOwner = Partial<Record<"cwd" | "pid" | "tool", unknown>>;
type LocalHeavyCheckLockParams = {
cwd: string;
env?: Env;
lockName?: string;
toolName: string;
};
/** Return whether local-heavy-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";
}
/** Ensure local check runs opt into safeguard environment outside CI. */
export function resolveLocalHeavyCheckEnv(env: Env = process.env) {
if (isCiLikeEnv(env) || isLocalCheckEnabled(env)) {
return env;
}
return {
...env,
OPENCLAW_LOCAL_CHECK: "1",
};
}
/** Mark every nested heavy-check wrapper as covered by one parent-held lock. */
export function withLocalHeavyCheckLockHeld(env: Env): Env {
return {
...env,
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "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 = {},
) {
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 a dependency-less worktree to the primary checkout toolchain selected above. */
export function ensureRepoToolNodeModulesLink(
toolPath: string,
{
cwd = process.cwd(),
fileExists = fs.existsSync,
resolveCommonDir = resolveGitCommonDir,
symlink = fs.symlinkSync,
platform = process.platform,
}: RepoToolOptions & {
symlink?: typeof fs.symlinkSync;
platform?: NodeJS.Platform;
} = {},
) {
const localNodeModules = path.resolve(cwd, "node_modules");
if (fileExists(localNodeModules)) {
return localNodeModules;
}
const commonDir = resolveCommonDir(cwd);
if (!commonDir || path.basename(commonDir) !== ".git") {
return null;
}
const primaryNodeModules = path.join(path.dirname(commonDir), "node_modules");
const toolNodeModules = path.dirname(path.dirname(path.resolve(toolPath)));
if (toolNodeModules !== path.resolve(primaryNodeModules) || !fileExists(primaryNodeModules)) {
return null;
}
try {
// Match run-vitest.mjs's hydrated-toolchain behavior: keep one stable link
// so compilers can resolve imports from worktree source paths.
symlink(primaryNodeModules, 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");
}
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,
);
}
const resolvedHostResources = resolveHostResources(hostResources);
if (shouldThrottleLocalHeavyChecks(nextEnv, resolvedHostResources, "auto")) {
insertBeforeSeparator(nextArgs, "--singleThreaded");
insertBeforeSeparator(nextArgs, "--checkers", "1");
applyThrottledGoRuntimeEnv(nextEnv, resolvedHostResources);
}
if (nextEnv.OPENCLAW_TSGO_PPROF_DIR && !hasFlag(nextArgs, "--pprofDir")) {
insertBeforeSeparator(nextArgs, "--pprofDir", nextEnv.OPENCLAW_TSGO_PPROF_DIR);
}
return { env: nextEnv, args: nextArgs };
}
/** Apply local 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 (shouldThrottleLocalHeavyChecks(nextEnv, 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 };
}
/** Decide whether an oxlint invocation needs the local heavy-check lock. */
export function shouldAcquireLocalHeavyCheckLockForOxlint(
args: string[],
{ cwd = process.cwd(), env = process.env }: { cwd?: string; env?: 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;
}
});
}
/** Decide whether a tsgo invocation needs the local heavy-check lock. */
export function shouldAcquireLocalHeavyCheckLockForTsgo(args: string[], env: 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",
);
}
function shouldThrottleLocalHeavyChecks(
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
);
}
/** Acquire a filesystem lock for one local heavy check and return its release callback. */
export function acquireLocalHeavyCheckLockSync(params: LocalHeavyCheckLockParams) {
const env = params.env ?? process.env;
if (!isLocalCheckEnabled(env)) {
return () => {};
}
const locksDir = resolveHeavyCheckLocksDir(params.cwd, env);
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,
"OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS",
);
const pollMs = readPositiveInt(
env.OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS,
DEFAULT_LOCK_POLL_MS,
"OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS",
);
const progressMs = readPositiveInt(
env.OPENCLAW_HEAVY_CHECK_LOCK_PROGRESS_MS,
DEFAULT_LOCK_PROGRESS_MS,
"OPENCLAW_HEAVY_CHECK_LOCK_PROGRESS_MS",
);
const staleLockMs = readPositiveInt(
env.OPENCLAW_HEAVY_CHECK_STALE_LOCK_MS,
DEFAULT_STALE_LOCK_MS,
"OPENCLAW_HEAVY_CHECK_STALE_LOCK_MS",
);
const startedAt = Date.now();
let waitLogBudget = 1;
let lastProgressAt = startedAt;
const consumeInitialWaitLog = () => waitLogBudget-- > 0;
const consumeProgressLog = (now: number) => {
if (now - lastProgressAt < progressMs) {
return false;
}
lastProgressAt = now;
return true;
};
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 (consumeInitialWaitLog()) {
const ownerLabel = describeOwner(owner);
console.error(
`[${params.toolName}] queued behind the local heavy-check lock${
ownerLabel ? ` held by ${ownerLabel}` : ""
}...`,
);
} else if (consumeProgressLog(Date.now())) {
const ownerLabel = describeOwner(owner);
console.error(
`[${params.toolName}] still waiting ${formatElapsedMs(elapsedMs)} for the local heavy-check lock${
ownerLabel ? ` held by ${ownerLabel}` : ""
}...`,
);
}
sleepSync(pollMs);
}
}
}
function resolveHeavyCheckLocksDir(cwd: string, env: Env) {
const lockScope = env.OPENCLAW_HEAVY_CHECK_LOCK_SCOPE?.trim().toLowerCase();
if (lockScope === "worktree") {
return path.join(resolveGitWorktreeRoot(cwd), ".artifacts", "openclaw-local-checks");
}
return path.join(resolveGitCommonDir(cwd), "openclaw-local-checks");
}
function resolveGitWorktreeRoot(cwd: string) {
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
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 cwd;
}
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 cleanupLegacyLockDirs(locksDir: string, staleLockMs: number) {
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, legacyLockDir, staleLockMs)) {
fs.rmSync(legacyLockDir, { recursive: true, force: true });
}
}
}
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,
};
}
function readPositiveInt(rawValue: string | undefined, fallback: number, label: string) {
const text = rawValue?.trim();
if (!text) {
return fallback;
}
if (!/^\d+$/u.test(text)) {
throw new Error(`${label} must be a positive integer; got: ${rawValue}`);
}
const parsed = Number(text);
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
throw new Error(`${label} must be a positive integer; got: ${rawValue}`);
}
return parsed;
}
function writeOwnerFile(ownerPath: string, owner: unknown) {
fs.writeFileSync(ownerPath, `${JSON.stringify(owner, null, 2)}\n`, "utf8");
}
function readOwnerFile(ownerPath: string): LockOwner | null {
try {
return JSON.parse(fs.readFileSync(ownerPath, "utf8"));
} catch {
return null;
}
}
function isAlreadyExistsError(error: unknown) {
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
}
function shouldReclaimLock(owner: LockOwner | null, lockDir: string, staleLockMs: number) {
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: number) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
}
}
function describeOwner(owner: LockOwner | null) {
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: number) {
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: number) {
Atomics.wait(SLEEP_BUFFER, 0, 0, ms);
}
-75
View File
@@ -1,75 +0,0 @@
// Holds the shared local heavy-check lock for a whole scripts/pr gate block so
// concurrent gate runs across .worktrees serialize before their first command
// instead of dying mid-test on child lock timeouts or no-output watchdog kills.
import fs from "node:fs";
import {
acquireLocalHeavyCheckLockSync,
resolveLocalHeavyCheckEnv,
} from "./lib/local-heavy-check-runtime.mts";
// A queued gate block legitimately waits out another full gate run; the
// 10-minute per-command default in the lock runtime is far too short for that.
const DEFAULT_GATE_LOCK_TIMEOUT_MS = 2 * 60 * 60 * 1000;
const PARENT_WATCH_INTERVAL_MS = 500;
function parseArgs(argv: string[]): { statusFile: string } {
const args = { statusFile: "" };
for (let index = 0; index < argv.length; index += 1) {
if (argv[index] === "--status-file") {
args.statusFile = argv[index + 1] ?? "";
index += 1;
continue;
}
throw new Error(`Unknown option: ${argv[index]}`);
}
if (!args.statusFile) {
throw new Error("Usage: node scripts/pr-gates-lock.mts --status-file <path>");
}
return args;
}
function main(): void {
const { statusFile } = parseArgs(process.argv.slice(2));
const baseEnv = resolveLocalHeavyCheckEnv(process.env);
const env = baseEnv.OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS
? baseEnv
: { ...baseEnv, OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS: String(DEFAULT_GATE_LOCK_TIMEOUT_MS) };
const parentPid = process.ppid;
const release = acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "pr-gates",
});
let released = false;
const releaseOnce = () => {
if (released) {
return;
}
released = true;
release();
};
process.on("exit", releaseOnce);
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
process.on(signal, () => {
releaseOnce();
process.exit(0);
});
}
fs.writeFileSync(statusFile, "acquired\n");
// The owner-pid reclaim in the lock runtime already covers a SIGKILLed
// holder; this watch releases within half a second when the gate shell dies.
// ppid 1 also counts as dead: orphans reparent to init/launchd, and a
// helper that started orphaned has no gate block to hold the lock for.
setInterval(() => {
if (process.ppid !== parentPid || process.ppid === 1) {
releaseOnce();
process.exit(0);
}
}, PARENT_WATCH_INTERVAL_MS);
}
main();
-56
View File
@@ -122,59 +122,11 @@ resolve_pr_gates_remote_mode() {
esac
}
PR_GATES_LOCK_PID=""
PR_GATES_LOCK_STATUS_FILE=""
acquire_pr_gates_lock() {
# Serialize whole gate blocks across .worktrees on the shared heavy-check
# lock; a queued gate run waits here, before its first command, instead of
# dying on child lock timeouts or shard no-output watchdog kills mid-test.
if [ "${OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD:-}" = "1" ]; then
return 0
fi
PR_GATES_LOCK_STATUS_FILE=$(mktemp)
# Use the canonical helper: the PR branch under test may predate it.
local scripts_dir="${script_parent_dir:-}"
if [ -z "$scripts_dir" ]; then
scripts_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
fi
node "$scripts_dir/pr-gates-lock.mts" --status-file "$PR_GATES_LOCK_STATUS_FILE" &
PR_GATES_LOCK_PID=$!
while [ ! -s "$PR_GATES_LOCK_STATUS_FILE" ]; do
if ! kill -0 "$PR_GATES_LOCK_PID" 2>/dev/null; then
wait "$PR_GATES_LOCK_PID" 2>/dev/null || true
PR_GATES_LOCK_PID=""
echo "Failed to acquire the shared local heavy-check lock for prepare gates."
exit 1
fi
sleep 0.2
done
# Same held-lock contract check-changed uses for its children: gate stages
# must not re-acquire the lock the block holder already owns.
export OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD=1
export OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD=1
export OPENCLAW_OXLINT_SKIP_LOCK=1
}
prepare_local_gate_workspace() {
pin_worktree_bundled_plugins_dir
acquire_pr_gates_lock
bootstrap_deps_if_needed
}
release_pr_gates_lock() {
if [ -z "${PR_GATES_LOCK_PID:-}" ]; then
return 0
fi
kill "$PR_GATES_LOCK_PID" 2>/dev/null || true
wait "$PR_GATES_LOCK_PID" 2>/dev/null || true
PR_GATES_LOCK_PID=""
rm -f "$PR_GATES_LOCK_STATUS_FILE"
PR_GATES_LOCK_STATUS_FILE=""
unset OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD OPENCLAW_OXLINT_SKIP_LOCK
}
run_remote_testbox_full_test_gate() {
local label="$1"
local log_file="$2"
@@ -320,7 +272,6 @@ run_prepare_push_retry_gates() {
local remote_gates_run_url=""
if [ "$docs_only" = "true" ]; then
release_pr_gates_lock
gates_mode="docs_only"
# No test ran: carry the prior full-gates proof and how it was produced.
full_gates_head="${FULL_GATES_HEAD_SHA:-}"
@@ -328,7 +279,6 @@ run_prepare_push_retry_gates() {
remote_gates_lease_id="${REMOTE_GATES_LEASE_ID:-}"
remote_gates_run_url="${REMOTE_GATES_RUN_URL:-}"
elif [ "$gates_remote_mode" = "testbox" ]; then
release_pr_gates_lock
gates_mode="remote_testbox"
run_remote_testbox_full_test_gate \
"pnpm test (lease-retry, blacksmith-testbox)" \
@@ -342,7 +292,6 @@ run_prepare_push_retry_gates() {
echo "Remote testbox lease-retry gate stamp: $remote_gates_lease_id${remote_gates_run_url:+ ($remote_gates_run_url)}"
else
run_quiet_logged "pnpm test (lease-retry)" ".local/lease-retry-test.log" pnpm test
release_pr_gates_lock
fi
write_gates_env_stamp \
@@ -476,7 +425,6 @@ prepare_gates() {
run_quiet_logged "pnpm check" ".local/gates-check.log" pnpm check
if [ "$docs_only" = "true" ]; then
release_pr_gates_lock
gates_mode="docs_only"
previous_full_gates_head=""
remote_gates_provider=""
@@ -484,9 +432,6 @@ prepare_gates() {
remote_gates_run_url=""
echo "Docs-only change detected with high confidence; skipping pnpm test."
elif [ "$gates_remote_mode" = "testbox" ]; then
# The full suite runs on a Blacksmith Testbox, so free the local lock
# for other heavy work while we wait on remote proof.
release_pr_gates_lock
gates_mode="remote_testbox"
echo "Running pnpm test on Blacksmith Testbox (OPENCLAW_PR_GATES_REMOTE=testbox)."
run_remote_testbox_full_test_gate \
@@ -512,7 +457,6 @@ prepare_gates() {
echo "Running pnpm test with host-aware scheduling defaults."
run_quiet_logged "pnpm test" ".local/gates-test.log" pnpm test
fi
release_pr_gates_lock
remote_gates_provider=""
remote_gates_lease_id=""
remote_gates_run_url=""
@@ -16,11 +16,12 @@ import {
MAX_TIMER_TIMEOUT_MS,
resolveTimerTimeoutMs,
} from "../packages/normalization-core/src/number-coercion.ts";
import { acquireExtensionPackageBoundaryArtifactLockSync } from "./lib/extension-package-boundary-artifact-lock.mts";
import {
ensureRepoToolNodeModulesLink,
isLocalCheckEnabled,
resolveRepoToolBinPath,
} from "./lib/local-heavy-check-runtime.mts";
} from "./lib/local-check-runtime.mts";
import { parsePositiveInt } from "./lib/numeric-options.mjs";
import {
listPluginSdkDeclarationOutputs,
@@ -989,7 +990,7 @@ export async function runNodeStepsInParallel(steps: NodeStep[]) {
}
/**
* Chooses serial or parallel artifact execution based on local heavy-check policy.
* Chooses serial or parallel artifact execution based on local check policy.
*/
export async function runNodeSteps(steps: NodeStep[], env: NodeJS.ProcessEnv = process.env) {
if (!isLocalCheckEnabled(env)) {
@@ -1108,7 +1109,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
prerequisiteSteps.push({
label: "plugin-sdk boundary dts",
args: [runTsgoScript, "-p", "tsconfig.plugin-sdk.dts.json", "--declaration", "true"],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: ROOT_BOUNDARY_TIMEOUT_MS,
stamp: {
path: ROOT_DTS_STAMP,
@@ -1127,7 +1127,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
prerequisiteSteps.push({
label: "plugin-sdk package boundary dts",
args: [runTsgoScript, "-p", "packages/plugin-sdk/tsconfig.json", "--declaration", "true"],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: ROOT_BOUNDARY_TIMEOUT_MS,
stamp: {
path: PACKAGE_DTS_STAMP,
@@ -1162,7 +1161,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/qa-channel/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stamp: {
path: QA_CHANNEL_DTS_STAMP,
@@ -1196,7 +1194,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/memory-core/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stamp: {
path: MEMORY_CORE_DTS_STAMP,
@@ -1230,7 +1227,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/matrix/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stamp: {
path: MATRIX_DTS_STAMP,
@@ -1264,7 +1260,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/discord/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stamp: {
path: DISCORD_DTS_STAMP,
@@ -1298,7 +1293,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/slack/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stamp: {
path: SLACK_DTS_STAMP,
@@ -1332,7 +1326,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/whatsapp/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stamp: {
path: WHATSAPP_DTS_STAMP,
@@ -1366,7 +1359,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/telegram/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stamp: {
path: TELEGRAM_DTS_STAMP,
@@ -1421,10 +1413,15 @@ async function main(argv: string[] = process.argv.slice(2)) {
}
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
process.exitCode = 1;
}
}
if (import.meta.main) {
await main();
const releaseArtifactLock = acquireExtensionPackageBoundaryArtifactLockSync(repoRoot);
try {
await main();
} finally {
releaseArtifactLock();
}
}
+24 -41
View File
@@ -6,12 +6,7 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
acquireLocalHeavyCheckLockSync,
applyLocalTsgoPolicy,
resolveRepoToolBinPath,
shouldAcquireLocalHeavyCheckLockForTsgo,
} from "./lib/local-heavy-check-runtime.mts";
import { applyLocalTsgoPolicy, resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
import { createManagedCommandInvocation } from "./lib/managed-child-process.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { TSGO_CORE_TEST_SHARDS, type TsgoCoreTestShard } from "./lib/tsgo-core-test-shards.mts";
@@ -170,43 +165,31 @@ function runTsgo(
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
totalMemoryBytes: os.totalmem(),
});
const releaseLock = shouldAcquireLocalHeavyCheckLockForTsgo(finalArgs, env)
? acquireLocalHeavyCheckLockSync({
cwd: repoRoot,
env,
toolName: "tsgo-profile",
})
: () => {};
const startedAt = Date.now();
try {
const tsgo = createManagedCommandInvocation({
args: finalArgs,
bin: tsgoPath,
env,
});
const result = spawnSync(tsgo.command, tsgo.args, {
cwd: repoRoot,
env,
encoding: "utf8",
maxBuffer: params.maxBuffer ?? 128 * 1024 * 1024,
shell: tsgo.shell,
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
});
const elapsedMs = Date.now() - startedAt;
const stdout = result.stdout ?? "";
const stderr = result.stderr ?? "";
if (result.error) {
throw result.error;
}
if ((result.status ?? 1) !== 0) {
const output = [stdout, stderr].filter(Boolean).join("\n");
throw new Error(`${label} failed with exit code ${result.status ?? 1}\n${output}`);
}
return { elapsedMs, stdout, stderr };
} finally {
releaseLock();
const tsgo = createManagedCommandInvocation({
args: finalArgs,
bin: tsgoPath,
env,
});
const result = spawnSync(tsgo.command, tsgo.args, {
cwd: repoRoot,
env,
encoding: "utf8",
maxBuffer: params.maxBuffer ?? 128 * 1024 * 1024,
shell: tsgo.shell,
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
});
const elapsedMs = Date.now() - startedAt;
const stdout = result.stdout ?? "";
const stderr = result.stderr ?? "";
if (result.error) {
throw result.error;
}
if ((result.status ?? 1) !== 0) {
const output = [stdout, stderr].filter(Boolean).join("\n");
throw new Error(`${label} failed with exit code ${result.status ?? 1}\n${output}`);
}
return { elapsedMs, stdout, stderr };
}
function parseDiagnostics(output: string): Diagnostics {
+1 -1
View File
@@ -6,7 +6,7 @@ import { pathToFileURL } from "node:url";
import {
ensureRepoToolNodeModulesLink,
resolveRepoToolBinPath,
} from "./lib/local-heavy-check-runtime.mts";
} from "./lib/local-check-runtime.mts";
function run(command: string, args: string[], options: SpawnSyncOptions) {
const result = spawnSync(command, args, options);
+51 -78
View File
@@ -4,12 +4,10 @@ import fs, { type Dirent } from "node:fs";
import os from "node:os";
import path from "node:path";
import {
acquireLocalHeavyCheckLockSync,
ensureRepoToolNodeModulesLink,
resolveLocalHeavyCheckEnv,
resolveLocalCheckEnv,
resolveRepoToolBinPath,
shouldAcquireLocalHeavyCheckLockForOxlint,
} from "./lib/local-heavy-check-runtime.mts";
} from "./lib/local-check-runtime.mts";
import { shouldPrepareExtensionPackageBoundaryArtifacts } from "./run-oxlint.mts";
const DEFAULT_WINDOWS_EXTENSION_CHUNK_SIZE = 8;
@@ -254,86 +252,62 @@ export async function main(
) {
const runner = path.resolve("scripts", "run-oxlint.mjs");
const shardArgs = parseShardRunnerArgs(extraArgs);
const env = resolveLocalHeavyCheckEnv(runtimeEnv);
const hasMetadataOnlyFlag = shardArgs.oxlintArgs.some((arg) =>
["--help", "-h", "--version", "-V", "--rules", "--print-config", "--init"].includes(arg),
const env = resolveLocalCheckEnv(runtimeEnv);
const shards = createOxlintShards({
cwd: process.cwd(),
env,
platform: process.platform,
splitCore: shardArgs.splitCore,
});
const selectedShards = selectCoreOxlintStripe(
filterOxlintShards(shards, shardArgs.only),
shardArgs.coreStripe,
);
const shouldAcquireParentLock =
!hasMetadataOnlyFlag ||
shouldAcquireLocalHeavyCheckLockForOxlint(shardArgs.oxlintArgs, {
cwd: process.cwd(),
env,
});
const releaseLock =
env.OPENCLAW_OXLINT_SKIP_LOCK === "1"
? () => {}
: shouldAcquireParentLock
? acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "oxlint shards",
})
: () => {};
try {
const shards = createOxlintShards({
cwd: process.cwd(),
ensureRepoToolNodeModulesLink(resolveRepoToolBinPath("oxlint"));
const prepareResult = shouldPrepareExtensionPackageBoundaryArtifactsForShards(
selectedShards,
shardArgs.oxlintArgs,
)
? spawnSync(
process.execPath,
[
"--import",
"tsx",
path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mts"),
],
{
stdio: "inherit",
env,
},
)
: undefined;
if (prepareResult?.error) {
throw prepareResult.error;
}
if (prepareResult && (prepareResult.status ?? 1) !== 0) {
process.exitCode = prepareResult.status ?? 1;
} else {
const shardConcurrency = resolveOxlintShardConcurrency({
env,
platform: process.platform,
splitCore: shardArgs.splitCore,
});
const selectedShards = selectCoreOxlintStripe(
filterOxlintShards(shards, shardArgs.only),
shardArgs.coreStripe,
const hostResources = resolveHostResources();
// stderr: stdout may carry machine-readable oxlint output for callers.
console.error(
`[oxlint] shard concurrency ${Math.max(1, Math.min(shardConcurrency, selectedShards.length))} ` +
`(cpus=${hostResources.logicalCpuCount}, memGB=${Math.round(hostResources.totalMemoryBytes / 1024 ** 3)})`,
);
ensureRepoToolNodeModulesLink(resolveRepoToolBinPath("oxlint"));
const prepareResult = shouldPrepareExtensionPackageBoundaryArtifactsForShards(
selectedShards,
shardArgs.oxlintArgs,
)
? spawnSync(
process.execPath,
[
"--import",
"tsx",
path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mts"),
],
{
stdio: "inherit",
env,
},
)
: undefined;
if (prepareResult?.error) {
throw prepareResult.error;
}
if (prepareResult && (prepareResult.status ?? 1) !== 0) {
process.exitCode = prepareResult.status ?? 1;
} else {
const shardConcurrency = resolveOxlintShardConcurrency({
env,
platform: process.platform,
splitCore: shardArgs.splitCore,
});
const hostResources = resolveHostResources();
// stderr: stdout may carry machine-readable oxlint output for callers.
console.error(
`[oxlint] shard concurrency ${Math.max(1, Math.min(shardConcurrency, selectedShards.length))} ` +
`(cpus=${hostResources.logicalCpuCount}, memGB=${Math.round(hostResources.totalMemoryBytes / 1024 ** 3)})`,
);
const results = await runShards({
concurrency: Math.max(1, Math.min(shardConcurrency, selectedShards.length)),
entries: selectedShards,
env,
extraArgs: shardArgs.oxlintArgs,
runner,
});
process.exitCode = results.find((status) => status !== 0) ?? 0;
}
} finally {
releaseLock();
const results = await runShards({
concurrency: Math.max(1, Math.min(shardConcurrency, selectedShards.length)),
entries: selectedShards,
env,
extraArgs: shardArgs.oxlintArgs,
runner,
});
process.exitCode = results.find((status) => status !== 0) ?? 0;
}
}
@@ -540,7 +514,6 @@ export async function runShard({ env, extraArgs, runner, shard }: ShardRunnerOpt
detached: useProcessGroup,
env: {
...env,
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_OXLINT_SKIP_PREPARE: "1",
},
});
+18 -51
View File
@@ -1,16 +1,14 @@
// Runs oxlint with local heavy-check policy, sparse-checkout filtering, and
// Runs oxlint with local resource policy, sparse-checkout filtering, and
// plugin package-boundary artifact preparation when needed.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
acquireLocalHeavyCheckLockSync,
applyLocalOxlintPolicy,
resolveLocalHeavyCheckEnv,
resolveLocalCheckEnv,
resolveRepoToolBinPath,
shouldAcquireLocalHeavyCheckLockForOxlint,
} from "./lib/local-heavy-check-runtime.mts";
} from "./lib/local-check-runtime.mts";
import { createManagedCommandInvocation, runManagedCommand } from "./lib/managed-child-process.mts";
import { resolvePathEnvKey } from "./windows-cmd-helpers.mjs";
@@ -227,27 +225,14 @@ function resolveOxlintToolchainEnv(
}
async function prepareExtensionPackageBoundaryArtifacts(env: NodeJS.ProcessEnv) {
const releaseArtifactsLock = acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
const status = await runManagedCommand({
bin: process.execPath,
args: PREPARE_EXTENSION_BOUNDARY_ARGS,
env,
toolName: "extension-package-boundary-artifacts",
lockName: "extension-package-boundary-artifacts",
});
try {
const status = await runManagedCommand({
bin: process.execPath,
args: PREPARE_EXTENSION_BOUNDARY_ARGS,
env,
});
if (status !== 0) {
throw new Error(
`prepare-extension-package-boundary-artifacts failed with exit code ${status}`,
);
}
} finally {
releaseArtifactsLock();
if (status !== 0) {
throw new Error(`prepare-extension-package-boundary-artifacts failed with exit code ${status}`);
}
}
@@ -260,7 +245,7 @@ async function main(
) {
const focusedConfig = argv.includes(OPENCLAW_FOCUSED_CONFIG_FLAG);
const oxlintArgs = argv.filter((arg) => arg !== OPENCLAW_FOCUSED_CONFIG_FLAG);
const localEnv = resolveLocalHeavyCheckEnv(runtimeEnv);
const localEnv = resolveLocalCheckEnv(runtimeEnv);
// Focused configs are syntax-only guards; keep wrapper process handling
// without the broad type-aware policy or package artifact preparation.
const { args: policyArgs, env } = focusedConfig
@@ -292,34 +277,16 @@ async function main(
return;
}
const releaseLock =
env.OPENCLAW_OXLINT_SKIP_LOCK === "1" || focusedConfig
? () => {}
: shouldAcquireLocalHeavyCheckLockForOxlint(finalArgs, {
cwd: process.cwd(),
env,
})
? acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "oxlint",
})
: () => {};
try {
if (needsArtifactPreparation) {
await prepareExtensionPackageBoundaryArtifacts(env);
}
const status = await runManagedCommand({
bin: oxlintPath,
args: finalArgs,
env: resolveOxlintToolchainEnv(oxlintPath, env),
});
process.exitCode = status;
} finally {
releaseLock();
if (needsArtifactPreparation) {
await prepareExtensionPackageBoundaryArtifacts(env);
}
const status = await runManagedCommand({
bin: oxlintPath,
args: finalArgs,
env: resolveOxlintToolchainEnv(oxlintPath, env),
});
process.exitCode = status;
}
if (import.meta.main) {
+1 -1
View File
@@ -4,7 +4,7 @@ import path from "node:path";
import {
ensureRepoToolNodeModulesLink,
resolveRepoToolBinPath,
} from "./lib/local-heavy-check-runtime.mts";
} from "./lib/local-check-runtime.mts";
const stylelintPath = resolveRepoToolBinPath("stylelint");
ensureRepoToolNodeModulesLink(stylelintPath);
+20 -35
View File
@@ -1,13 +1,10 @@
#!/usr/bin/env node
// Run bounded test graphs in fresh processes so one shard's checker heap cannot
// accumulate while the next shard loads. Hold one lock across the full sequence.
// accumulate while the next shard loads.
import { spawn, type ChildProcess } from "node:child_process";
import path from "node:path";
import {
acquireLocalHeavyCheckLockSync,
resolveLocalHeavyCheckEnv,
} from "./lib/local-heavy-check-runtime.mts";
import { resolveLocalCheckEnv } from "./lib/local-check-runtime.mts";
import { signalExitCode } from "./lib/managed-child-process.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
@@ -48,15 +45,7 @@ if (concurrencyFlagIndex >= 0) {
process.exit(1);
}
}
const env = resolveLocalHeavyCheckEnv(process.env);
const releaseLock =
env.OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD === "1"
? () => {}
: acquireLocalHeavyCheckLockSync({
cwd: repoRoot,
env,
toolName: "tsgo:core:test",
});
const env = resolveLocalCheckEnv(process.env);
function runShard(config: string): Promise<number> {
return new Promise((resolve, reject) => {
@@ -65,7 +54,7 @@ function runShard(config: string): Promise<number> {
[path.join(repoRoot, "scripts/run-tsgo.mjs"), "-b", config, "--builders", "1"],
{
cwd: repoRoot,
env: { ...env, OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
env,
stdio: "inherit",
},
);
@@ -76,26 +65,22 @@ function runShard(config: string): Promise<number> {
});
}
try {
const queue = [...shards];
let failureCode = 0;
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
for (;;) {
const shard = queue.shift();
// Stop draining after the first failure so the exit stays prompt.
if (!shard || failureCode !== 0) {
return;
}
const code = await runShard(shard.config);
if (code !== 0 && failureCode === 0) {
failureCode = code;
}
const queue = [...shards];
let failureCode = 0;
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
for (;;) {
const shard = queue.shift();
// Stop draining after the first failure so the exit stays prompt.
if (!shard || failureCode !== 0) {
return;
}
const code = await runShard(shard.config);
if (code !== 0 && failureCode === 0) {
failureCode = code;
}
});
await Promise.all(workers);
if (failureCode !== 0) {
process.exitCode = failureCode;
}
} finally {
releaseLock();
});
await Promise.all(workers);
if (failureCode !== 0) {
process.exitCode = failureCode;
}
+30 -46
View File
@@ -1,17 +1,15 @@
// Runs tsgo through local heavy-check policy and sparse-checkout guards.
// Runs tsgo through local resource policy and sparse-checkout guards.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { readFlagValue } from "./lib/arg-utils.mts";
import {
acquireLocalHeavyCheckLockSync,
applyLocalTsgoPolicy,
ensureRepoToolNodeModulesLink,
resolveLocalHeavyCheckEnv,
resolveLocalCheckEnv,
resolveRepoToolBinPath,
shouldAcquireLocalHeavyCheckLockForTsgo,
} from "./lib/local-heavy-check-runtime.mts";
} from "./lib/local-check-runtime.mts";
import { createManagedCommandInvocation } from "./lib/managed-child-process.mts";
import {
getSparseTsgoGuardError,
@@ -26,7 +24,7 @@ function main(): void {
};
const { args: finalArgs, env } = applyLocalTsgoPolicy(
process.argv.slice(2),
resolveLocalHeavyCheckEnv(process.env),
resolveLocalCheckEnv(process.env),
hostResources,
);
@@ -36,49 +34,35 @@ function main(): void {
fs.mkdirSync(path.dirname(path.resolve(tsBuildInfoFile)), { recursive: true });
}
const sparseGuardError = getSparseTsgoGuardError(finalArgs, { cwd: process.cwd() });
const releaseLock =
sparseGuardError ||
env.OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD === "1" ||
!shouldAcquireLocalHeavyCheckLockForTsgo(finalArgs, env)
? () => {}
: acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "tsgo",
});
try {
if (sparseGuardError) {
console.error(sparseGuardError);
if (shouldSkipSparseTsgoGuardError(env)) {
console.error("[tsgo] skipping sparse-missing project because OPENCLAW_TSGO_SPARSE_SKIP=1");
process.exitCode = 0;
} else {
process.exitCode = 1;
}
if (sparseGuardError) {
console.error(sparseGuardError);
if (shouldSkipSparseTsgoGuardError(env)) {
console.error("[tsgo] skipping sparse-missing project because OPENCLAW_TSGO_SPARSE_SKIP=1");
process.exitCode = 0;
} else {
ensureRepoToolNodeModulesLink(tsgoPath);
const tsgo = createManagedCommandInvocation({
args: finalArgs,
bin: tsgoPath,
env,
});
const result = spawnSync(tsgo.command, tsgo.args, {
stdio: "inherit",
env,
shell: tsgo.shell,
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
});
if (result.error) {
throw result.error;
}
process.exitCode = result.status ?? 1;
process.exitCode = 1;
}
} finally {
releaseLock();
return;
}
ensureRepoToolNodeModulesLink(tsgoPath);
const tsgo = createManagedCommandInvocation({
args: finalArgs,
bin: tsgoPath,
env,
});
const result = spawnSync(tsgo.command, tsgo.args, {
stdio: "inherit",
env,
shell: tsgo.shell,
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
});
if (result.error) {
throw result.error;
}
process.exitCode = result.status ?? 1;
}
if (import.meta.main) {
+1 -39
View File
@@ -5,10 +5,6 @@ import fs from "node:fs";
import { performance } from "node:perf_hooks";
import pMap from "p-map";
import { formatMs } from "./lib/check-timing-summary.mts";
import {
acquireLocalHeavyCheckLockSync,
withLocalHeavyCheckLockHeld,
} from "./lib/local-heavy-check-runtime.mts";
import {
isCiLikeEnv,
resolveLocalFullSuiteProfile,
@@ -42,7 +38,6 @@ import {
resolveParallelFullSuiteConcurrency,
resolveChangedTestTargetPlanForArgs,
resolveChangedTargetArgs,
shouldAcquireLocalHeavyCheckLock,
shouldRetryVitestNoOutputTimeout,
type FailedVitestShard,
type VitestRunSpec as BaseVitestRunSpec,
@@ -59,19 +54,6 @@ type VitestCommandOutcome = {
type ShardTiming = NonNullable<ReturnType<typeof createShardTimingSample>>;
// Keep this shim so `pnpm test -- src/foo.test.ts` still forwards filters
// cleanly instead of leaking pnpm's passthrough sentinel to Vitest.
let releaseLock = () => {};
let lockReleased = false;
const releaseLockOnce = () => {
if (lockReleased) {
return;
}
lockReleased = true;
releaseLock();
};
function isWrapperMetadataRequest(args: string[]) {
for (const arg of args) {
if (arg === "--") {
@@ -193,7 +175,6 @@ async function runLoggedVitestSpec(spec: VitestRunSpec) {
}
if (result.signal) {
console.error(`[test] ${spec.config} exited by signal ${result.signal}`);
releaseLockOnce();
process.kill(process.pid, result.signal);
return null;
}
@@ -312,7 +293,7 @@ async function main() {
baseEnv,
cwd: process.cwd(),
});
let runSpecs = applyDefaultMultiSpecVitestCachePaths(
const runSpecs = applyDefaultMultiSpecVitestCachePaths(
applyDefaultVitestNoOutputTimeout(
applyFullExtensionsHeapBudget(rawRunSpecs, { env: baseEnv }),
{
@@ -328,21 +309,6 @@ async function main() {
return;
}
const acquiresLocalHeavyCheckLock = shouldAcquireLocalHeavyCheckLock(runSpecs, baseEnv);
releaseLock = acquiresLocalHeavyCheckLock
? acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env: baseEnv,
toolName: "test",
})
: () => {};
if (acquiresLocalHeavyCheckLock || baseEnv.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD === "1") {
runSpecs = runSpecs.map((spec) => ({
...spec,
env: withLocalHeavyCheckLockHeld(spec.env),
}));
}
const isFullSuiteRun =
targetArgs.length === 0 &&
changedTargetArgs === null &&
@@ -390,7 +356,6 @@ async function main() {
for (const line of formatFailedShardDigest(failures)) {
console.error(line);
}
releaseLockOnce();
if (parallelExitCode !== 0) {
process.exitCode = parallelExitCode;
}
@@ -412,7 +377,6 @@ async function main() {
exitCode = exitCode || result.code;
if (spec.continueOnFailure !== true) {
printTestSummary("failed", timings.length, performance.now() - suiteStartedAt);
releaseLockOnce();
process.exitCode = result.code;
return;
}
@@ -425,7 +389,6 @@ async function main() {
performance.now() - suiteStartedAt,
);
releaseLockOnce();
if (exitCode !== 0) {
process.exitCode = exitCode;
}
@@ -444,7 +407,6 @@ function printTestSummary(
}
main().catch((error: unknown) => {
releaseLockOnce();
console.error(error);
process.exitCode = 1;
});
-25
View File
@@ -4223,31 +4223,6 @@ function filterPlansForContractIncludeFile(plans: VitestRunPlan[], env: NodeJS.P
});
}
export function shouldAcquireLocalHeavyCheckLock(
runSpecs: Array<Pick<VitestRunSpec, "config" | "includePatterns" | "watchMode">>,
env = process.env,
) {
if (env.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD === "1") {
return false;
}
if (env.OPENCLAW_TEST_PROJECTS_FORCE_LOCK === "1") {
return true;
}
const runSpec = runSpecs.length === 1 ? runSpecs[0] : undefined;
if (!runSpec) {
return true;
}
return !(
(runSpec.config === TOOLING_VITEST_CONFIG ||
runSpec.config === TOOLING_ISOLATED_VITEST_CONFIG) &&
!runSpec.watchMode &&
Array.isArray(runSpec.includePatterns) &&
runSpec.includePatterns.length > 0
);
}
function expandVitestIncludePatterns(includePatterns: string[], cwd: string) {
const candidateFiles = includePatterns.some(isGlobTarget)
? listExplicitTestTargetFilesForCwd(cwd)