Fix agents
This commit is contained in:
@@ -112,6 +112,12 @@ pnpm verify:workspace # deep opt-in verification (lint -> test:full -> build);
|
|||||||
|
|
||||||
Never kill processes on port 4040 and never start test servers on 4040. Use `--port 0` or another free port.
|
Never kill processes on port 4040 and never start test servers on 4040. Use `--port 0` or another free port.
|
||||||
|
|
||||||
|
### Never run an unbounded `find` against the system temp directory
|
||||||
|
|
||||||
|
Do not issue a recursive `find` (or any unbounded recursive directory walk) rooted at the OS temp directory — `$TMPDIR`, `/tmp`, or macOS `/var/folders/...` (canonical `/private/var/...`). The temp root can hold an enormous number of entries on CI and long-lived dev hosts, so a broad scan can hang for minutes and pin I/O.
|
||||||
|
|
||||||
|
When you need a Fusion temp artifact, target the known prefix directly and list a single level with a prefix filter — never walk the whole temp tree. The canonical bounded pattern is the engine's own sweep: a non-recursive `readdirSync(tmpdir())` filtered by a known prefix such as `fusion-ai-merge-` (`SelfHealingManager.cleanupStaleTempMergeWorktrees()` in `packages/engine/src/self-healing.ts`). Scoped `find` calls under a project worktree or `.fusion/` are fine; only the broad temp-root scan is forbidden.
|
||||||
|
|
||||||
### Engine Process Rules
|
### Engine Process Rules
|
||||||
|
|
||||||
#### Never use `execSync` for user-configured commands
|
#### Never use `execSync` for user-configured commands
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const {
|
|||||||
mkdtempSync,
|
mkdtempSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
|
readdirSync,
|
||||||
rmSync,
|
rmSync,
|
||||||
realpathSync,
|
realpathSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
@@ -192,20 +193,32 @@ function isProcessAlive(pid: number): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeTmpdirRedirectSinkForPid(ownerPid: number): void {
|
||||||
|
try {
|
||||||
|
rmSync(join(WORKER_ROOT, `redir-${ownerPid}`), { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// Ignore stale-sink cleanup failures; global teardown still owns WORKER_ROOT.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sweepDeadTmpdirRedirectSinks(): void {
|
function sweepDeadTmpdirRedirectSinks(): void {
|
||||||
if (tmpdirRedirectSweepComplete) return;
|
if (tmpdirRedirectSweepComplete) return;
|
||||||
tmpdirRedirectSweepComplete = true;
|
tmpdirRedirectSweepComplete = true;
|
||||||
|
|
||||||
let ownerPids: number[];
|
// Registry-backed cleanup avoids scanning the OS temp root while still
|
||||||
|
// reclaiming redirect sinks from fork-pool workers that were hard-killed.
|
||||||
|
let ownerPids: number[] = [];
|
||||||
try {
|
try {
|
||||||
ownerPids = Array.from(new Set(
|
ownerPids = Array.from(new Set(
|
||||||
readFileSync(TMPDIR_REDIRECT_REGISTRY, "utf8")
|
readFileSync(TMPDIR_REDIRECT_REGISTRY, "utf8")
|
||||||
.split(/\r?\n/)
|
.split(/
|
||||||
|
?
|
||||||
/)
|
/)
|
||||||
.map((line) => Number.parseInt(line, 10))
|
.map((line) => Number.parseInt(line, 10))
|
||||||
.filter((pid) => Number.isInteger(pid) && pid > 0),
|
.filter((pid) => Number.isInteger(pid) && pid > 0),
|
||||||
));
|
));
|
||||||
return;
|
} catch {
|
||||||
|
// The registry may not exist yet. The bounded WORKER_ROOT sweep below still
|
||||||
// catches legacy redirect dirs created before the registry was introduced.
|
// catches legacy redirect dirs created before the registry was introduced.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,15 +228,30 @@ function sweepDeadTmpdirRedirectSinks(): void {
|
|||||||
liveOwnerPids.push(ownerPid);
|
liveOwnerPids.push(ownerPid);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
rmSync(join(WORKER_ROOT, `redir-${ownerPid}`), { recursive: true, force: true });
|
|
||||||
} catch {
|
|
||||||
// Ignore stale-sink cleanup failures; global teardown still owns WORKER_ROOT.
|
|
||||||
removeTmpdirRedirectSinkForPid(ownerPid);
|
removeTmpdirRedirectSinkForPid(ownerPid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve the local self-healing behavior for redirect dirs that predate the
|
||||||
|
// registry or whose registry append was skipped. This is a single-level scan
|
||||||
|
// of WORKER_ROOT (not the OS temp root) and only touches dead pid-owned dirs.
|
||||||
|
try {
|
||||||
|
for (const entry of readdirSync(WORKER_ROOT)) {
|
||||||
|
const match = /^redir-(\d+)$/.exec(entry);
|
||||||
|
if (!match) continue;
|
||||||
|
const ownerPid = Number.parseInt(match[1], 10);
|
||||||
|
if (ownerPid === process.pid || liveOwnerPids.includes(ownerPid) || isProcessAlive(ownerPid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
removeTmpdirRedirectSinkForPid(ownerPid);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
// Best-effort only; stale entries are harmless and swept by future workers.
|
// Best-effort only; stale entries are harmless and swept by future workers.
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(TMPDIR_REDIRECT_REGISTRY, liveOwnerPids.length > 0 ? `${liveOwnerPids.join("\n")}\n` : "");
|
try {
|
||||||
|
writeFileSync(TMPDIR_REDIRECT_REGISTRY, liveOwnerPids.length > 0 ? `${liveOwnerPids.join("
|
||||||
|
")}
|
||||||
` : "");
|
` : "");
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort only; stale entries are harmless and swept by future workers.
|
// Best-effort only; stale entries are harmless and swept by future workers.
|
||||||
@@ -236,7 +264,8 @@ function ensureTmpdirRedirectSink(): string {
|
|||||||
sweepDeadTmpdirRedirectSinks();
|
sweepDeadTmpdirRedirectSinks();
|
||||||
const sink = join(WORKER_ROOT, `redir-${process.pid}`);
|
const sink = join(WORKER_ROOT, `redir-${process.pid}`);
|
||||||
mkdirSync(sink, { recursive: true });
|
mkdirSync(sink, { recursive: true });
|
||||||
appendFileSync(TMPDIR_REDIRECT_REGISTRY, `${process.pid}\n`);
|
try {
|
||||||
|
appendFileSync(TMPDIR_REDIRECT_REGISTRY, `${process.pid}
|
||||||
`);
|
`);
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort only; the process exit hook and global teardown still clean up.
|
// Best-effort only; the process exit hook and global teardown still clean up.
|
||||||
@@ -256,6 +285,11 @@ function ensureTmpdirRedirectSink(): string {
|
|||||||
|
|
||||||
return sink;
|
return sink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a mkdtemp prefix points straight at the OS temp root, rewrite it into a
|
||||||
|
* swept per-process sink under WORKER_ROOT. Prefixes already nested under a
|
||||||
|
* subdirectory pass through unchanged, as do non-string prefixes (Buffer/URL).
|
||||||
*/
|
*/
|
||||||
function redirectTmpdirPrefix<T>(prefix: T): T {
|
function redirectTmpdirPrefix<T>(prefix: T): T {
|
||||||
if (typeof prefix !== "string") return prefix;
|
if (typeof prefix !== "string") return prefix;
|
||||||
@@ -264,6 +298,7 @@ function redirectTmpdirPrefix<T>(prefix: T): T {
|
|||||||
if (parent !== tmpdir() && parent !== REAL_TMPDIR) return prefix;
|
if (parent !== tmpdir() && parent !== REAL_TMPDIR) return prefix;
|
||||||
|
|
||||||
return join(ensureTmpdirRedirectSink(), basename(prefix)) as T;
|
return join(ensureTmpdirRedirectSink(), basename(prefix)) as T;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureIsolatedHome(): void {
|
function ensureIsolatedHome(): void {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
"reason": "Flake: vi.mock('node:child_process') occasionally doesn't take under workspace-concurrent runs, letting real git binary leak and report staged files unrelated to test scope (trips FileScopeViolationError). Same logic covered by real-git fixture tests in reliability-interactions/workflow-and-file-scope. FN-6206.",
|
"reason": "Flake: vi.mock('node:child_process') occasionally doesn't take under workspace-concurrent runs, letting real git binary leak and report staged files unrelated to test scope (trips FileScopeViolationError). Same logic covered by real-git fixture tests in reliability-interactions/workflow-and-file-scope. FN-6206.",
|
||||||
"quarantinedAt": "2026-06-10"
|
"quarantinedAt": "2026-06-10"
|
||||||
},
|
},
|
||||||
|
<<<<<<< Updated upstream
|
||||||
|
=======
|
||||||
|
|
||||||
|
>>>>>>> Stashed changes
|
||||||
{
|
{
|
||||||
"file": "packages/engine/src/__tests__/merger-ai-cleanup.test.ts",
|
"file": "packages/engine/src/__tests__/merger-ai-cleanup.test.ts",
|
||||||
"reason": "Flake observed during FN-6206 verification: `pruneExistingAiMergeWorktrees skips active-session paths` failed in full `pnpm --filter @fusion/engine test` runs while the file passed standalone, indicating suite-order/concurrency sensitivity. Follow-up FN-6207.",
|
"reason": "Flake observed during FN-6206 verification: `pruneExistingAiMergeWorktrees skips active-session paths` failed in full `pnpm --filter @fusion/engine test` runs while the file passed standalone, indicating suite-order/concurrency sensitivity. Follow-up FN-6207.",
|
||||||
|
|||||||
Reference in New Issue
Block a user