@kynver-app/runtime 0.1.120 → 0.1.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/anthropic-credentials.d.ts +3 -1
- package/dist/chat/chat-claim-loop.d.ts +3 -1
- package/dist/cleanup-duplicate-worktrees.d.ts +3 -2
- package/dist/cleanup-git-probe.d.ts +5 -0
- package/dist/cleanup-guards.d.ts +4 -0
- package/dist/cleanup-retention-config.d.ts +2 -0
- package/dist/cleanup-run-liveness.d.ts +3 -6
- package/dist/cleanup-salvage-evidence.d.ts +33 -0
- package/dist/cleanup-types.d.ts +2 -0
- package/dist/cleanup-worker-harness-live.d.ts +9 -0
- package/dist/cleanup-worktree-index.d.ts +2 -0
- package/dist/cli.js +884 -514
- package/dist/cli.js.map +4 -4
- package/dist/config.d.ts +22 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +835 -463
- package/dist/index.js.map +4 -4
- package/dist/run-metadata-retention.d.ts +15 -0
- package/dist/server/cleanup.js +452 -308
- package/dist/server/cleanup.js.map +4 -4
- package/dist/server/default-repo.js.map +2 -2
- package/dist/server/monitor.js.map +2 -2
- package/dist/server/worker-policy.js.map +2 -2
- package/dist/stale-reconcile.d.ts +2 -0
- package/dist/start.d.ts +6 -0
- package/package.json +2 -2
package/dist/server/cleanup.js
CHANGED
|
@@ -317,23 +317,23 @@ function isWslHost() {
|
|
|
317
317
|
function observeWslHostDisk(options = {}) {
|
|
318
318
|
const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
|
|
319
319
|
if (!wsl) return null;
|
|
320
|
-
const
|
|
320
|
+
const path26 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
|
|
321
321
|
const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
|
|
322
322
|
const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
|
|
323
323
|
const statfs = options.statfs ?? statfsSync;
|
|
324
324
|
let stats;
|
|
325
325
|
try {
|
|
326
|
-
stats = statfs(
|
|
326
|
+
stats = statfs(path26);
|
|
327
327
|
} catch (error) {
|
|
328
328
|
return {
|
|
329
329
|
ok: false,
|
|
330
|
-
path:
|
|
330
|
+
path: path26,
|
|
331
331
|
freeBytes: 0,
|
|
332
332
|
totalBytes: 0,
|
|
333
333
|
usedPercent: 100,
|
|
334
334
|
warnBelowBytes,
|
|
335
335
|
criticalBelowBytes,
|
|
336
|
-
reason: `Windows host disk probe failed at ${
|
|
336
|
+
reason: `Windows host disk probe failed at ${path26}: ${error.message}`,
|
|
337
337
|
probeError: error.message
|
|
338
338
|
};
|
|
339
339
|
}
|
|
@@ -347,11 +347,11 @@ function observeWslHostDisk(options = {}) {
|
|
|
347
347
|
let reason = null;
|
|
348
348
|
if (!ok) {
|
|
349
349
|
const tag = criticalFree ? "critical" : "warning";
|
|
350
|
-
reason = `Windows host disk ${
|
|
350
|
+
reason = `Windows host disk ${path26} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
|
|
351
351
|
}
|
|
352
352
|
return {
|
|
353
353
|
ok,
|
|
354
|
-
path:
|
|
354
|
+
path: path26,
|
|
355
355
|
freeBytes,
|
|
356
356
|
totalBytes,
|
|
357
357
|
usedPercent,
|
|
@@ -377,12 +377,12 @@ var init_wsl_host = __esm({
|
|
|
377
377
|
// src/disk-gate.ts
|
|
378
378
|
import { statfsSync as statfsSync2 } from "node:fs";
|
|
379
379
|
function observeRunnerDiskGate(input = {}) {
|
|
380
|
-
const
|
|
380
|
+
const path26 = input.diskPath?.trim() || "/";
|
|
381
381
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
382
382
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
383
383
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
384
384
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
385
|
-
const stats = statfsSync2(
|
|
385
|
+
const stats = statfsSync2(path26);
|
|
386
386
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
387
387
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
388
388
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -405,7 +405,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
405
405
|
}
|
|
406
406
|
return {
|
|
407
407
|
ok,
|
|
408
|
-
path:
|
|
408
|
+
path: path26,
|
|
409
409
|
freeBytes,
|
|
410
410
|
totalBytes,
|
|
411
411
|
usedPercent,
|
|
@@ -1917,11 +1917,11 @@ var init_paths = __esm({
|
|
|
1917
1917
|
|
|
1918
1918
|
// src/cleanup.ts
|
|
1919
1919
|
init_paths();
|
|
1920
|
-
import
|
|
1920
|
+
import path25 from "node:path";
|
|
1921
1921
|
|
|
1922
1922
|
// src/cleanup-guards.ts
|
|
1923
1923
|
init_landing_gate();
|
|
1924
|
-
import
|
|
1924
|
+
import path11 from "node:path";
|
|
1925
1925
|
|
|
1926
1926
|
// src/cleanup-index-status.ts
|
|
1927
1927
|
init_git();
|
|
@@ -2126,6 +2126,148 @@ function finalizeStaleRuns() {
|
|
|
2126
2126
|
// src/cleanup-run-liveness.ts
|
|
2127
2127
|
init_status();
|
|
2128
2128
|
|
|
2129
|
+
// src/cleanup-worker-harness-live.ts
|
|
2130
|
+
init_heartbeat();
|
|
2131
|
+
|
|
2132
|
+
// src/run-metadata-retention.ts
|
|
2133
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, statSync as statSync3 } from "node:fs";
|
|
2134
|
+
import path9 from "node:path";
|
|
2135
|
+
|
|
2136
|
+
// src/default-repo.ts
|
|
2137
|
+
init_config();
|
|
2138
|
+
init_default_repo_discovery();
|
|
2139
|
+
init_path_values();
|
|
2140
|
+
|
|
2141
|
+
// src/run-metadata-retention.ts
|
|
2142
|
+
init_heartbeat();
|
|
2143
|
+
init_paths();
|
|
2144
|
+
init_run_store();
|
|
2145
|
+
|
|
2146
|
+
// src/worker-metadata-paths.ts
|
|
2147
|
+
init_paths();
|
|
2148
|
+
init_util();
|
|
2149
|
+
import path8 from "node:path";
|
|
2150
|
+
var NESTED_RUNS = `${path8.sep}runs${path8.sep}runs${path8.sep}`;
|
|
2151
|
+
function workerArtifactPaths(workerDir) {
|
|
2152
|
+
return {
|
|
2153
|
+
workerJsonPath: path8.join(workerDir, "worker.json"),
|
|
2154
|
+
stdoutPath: path8.join(workerDir, "stdout.jsonl"),
|
|
2155
|
+
stderrPath: path8.join(workerDir, "stderr.log"),
|
|
2156
|
+
heartbeatPath: path8.join(workerDir, "heartbeat.jsonl"),
|
|
2157
|
+
lastStatusPath: path8.join(workerDir, "last-status.json")
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// src/run-metadata-retention.ts
|
|
2162
|
+
init_util();
|
|
2163
|
+
var RUN_METADATA_ACTIVE_SIGNAL_MS = 15 * 60 * 1e3;
|
|
2164
|
+
var TERMINAL_WORKER_ARCHIVE_AGE_MS = 60 * 60 * 1e3;
|
|
2165
|
+
function isHarnessRunMetadataPath(targetPath, harnessRoot) {
|
|
2166
|
+
const resolved = path9.resolve(targetPath);
|
|
2167
|
+
const runsDir = path9.resolve(harnessRunsDir(harnessRoot));
|
|
2168
|
+
const rel = path9.relative(runsDir, resolved);
|
|
2169
|
+
return rel !== ".." && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
2170
|
+
}
|
|
2171
|
+
function listRunDirIds(runsDir) {
|
|
2172
|
+
if (!existsSync9(runsDir)) return [];
|
|
2173
|
+
try {
|
|
2174
|
+
return readdirSync4(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== "runs").map((entry) => entry.name);
|
|
2175
|
+
} catch {
|
|
2176
|
+
return [];
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
function listWorkerNamesOnDisk(runDir2) {
|
|
2180
|
+
const workersDir = path9.join(runDir2, "workers");
|
|
2181
|
+
if (!existsSync9(workersDir)) return [];
|
|
2182
|
+
try {
|
|
2183
|
+
return readdirSync4(workersDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2184
|
+
} catch {
|
|
2185
|
+
return [];
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function pathRecentlyTouched(target, now, windowMs) {
|
|
2189
|
+
if (!existsSync9(target)) return false;
|
|
2190
|
+
try {
|
|
2191
|
+
const age = now - statSync3(target).mtimeMs;
|
|
2192
|
+
return Number.isFinite(age) && age >= 0 && age < windowMs;
|
|
2193
|
+
} catch {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
function workerDirHasActiveRetentionSignals(workerDir, now = Date.now(), windowMs = RUN_METADATA_ACTIVE_SIGNAL_MS) {
|
|
2198
|
+
if (!existsSync9(workerDir)) return false;
|
|
2199
|
+
const artifacts = workerArtifactPaths(workerDir);
|
|
2200
|
+
const worker = readJson(
|
|
2201
|
+
artifacts.workerJsonPath,
|
|
2202
|
+
void 0
|
|
2203
|
+
);
|
|
2204
|
+
if (worker?.status === "running" && isPidAlive(worker.pid)) return true;
|
|
2205
|
+
const heartbeat = parseHeartbeat(artifacts.heartbeatPath);
|
|
2206
|
+
if (heartbeat.lastHeartbeatAt) {
|
|
2207
|
+
const age = now - Date.parse(heartbeat.lastHeartbeatAt);
|
|
2208
|
+
if (Number.isFinite(age) && age >= 0 && age < windowMs) return true;
|
|
2209
|
+
}
|
|
2210
|
+
if (pathRecentlyTouched(artifacts.stdoutPath, now, windowMs)) return true;
|
|
2211
|
+
if (pathRecentlyTouched(artifacts.heartbeatPath, now, windowMs)) return true;
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
function runDirHasActiveRetentionSignals(runDir2, now = Date.now(), windowMs = RUN_METADATA_ACTIVE_SIGNAL_MS) {
|
|
2215
|
+
for (const name of listWorkerNamesOnDisk(runDir2)) {
|
|
2216
|
+
if (workerDirHasActiveRetentionSignals(path9.join(runDir2, "workers", name), now, windowMs)) {
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return false;
|
|
2221
|
+
}
|
|
2222
|
+
function collectFilesystemLiveRunKeys(harnessRoot, now = Date.now()) {
|
|
2223
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2224
|
+
const runsDir = harnessRunsDir(harnessRoot);
|
|
2225
|
+
for (const runId of listRunDirIds(runsDir)) {
|
|
2226
|
+
const runDir2 = path9.join(runsDir, runId);
|
|
2227
|
+
if (runDirHasActiveRetentionSignals(runDir2, now)) {
|
|
2228
|
+
keys.add(`${harnessRoot}\0${runId}`);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
return keys;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// src/cleanup-worker-harness-live.ts
|
|
2235
|
+
init_util();
|
|
2236
|
+
function heartbeatContentIsFresh(worker, now = Date.now(), windowMs = RUN_METADATA_ACTIVE_SIGNAL_MS) {
|
|
2237
|
+
const heartbeat = parseHeartbeat(worker.heartbeatPath);
|
|
2238
|
+
if (!heartbeat.lastHeartbeatAt) return false;
|
|
2239
|
+
const age = now - Date.parse(heartbeat.lastHeartbeatAt);
|
|
2240
|
+
return Number.isFinite(age) && age >= 0 && age < windowMs;
|
|
2241
|
+
}
|
|
2242
|
+
function isHarnessWorkerHarnessLive(worker, now = Date.now(), windowMs = RUN_METADATA_ACTIVE_SIGNAL_MS) {
|
|
2243
|
+
if (isPidAlive(worker.pid)) return true;
|
|
2244
|
+
return heartbeatContentIsFresh(worker, now, windowMs);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// src/cleanup-run-liveness.ts
|
|
2248
|
+
function deriveRunTerminal(indexed, ctx) {
|
|
2249
|
+
if (ctx) return ctx.runTerminalCache.derive(indexed.run);
|
|
2250
|
+
return deriveTerminalRunStatus(indexed.run);
|
|
2251
|
+
}
|
|
2252
|
+
function isWorkerProcessLive(indexed, now = Date.now()) {
|
|
2253
|
+
return isHarnessWorkerHarnessLive(indexed.worker, now);
|
|
2254
|
+
}
|
|
2255
|
+
function isRunStaleActive(indexed, ctx) {
|
|
2256
|
+
if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
|
|
2257
|
+
return deriveRunTerminal(indexed, ctx) !== null;
|
|
2258
|
+
}
|
|
2259
|
+
function runBlocksWorktreeRemoval(indexed, ctx, now = Date.now()) {
|
|
2260
|
+
if (isWorkerProcessLive(indexed, now)) return true;
|
|
2261
|
+
if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
|
|
2262
|
+
if (isRunStaleActive(indexed, ctx)) return false;
|
|
2263
|
+
const status = indexedWorktreeStatus(indexed);
|
|
2264
|
+
if (!isFinishedWorkerStatus(status)) {
|
|
2265
|
+
if (!isWorkerProcessLive(indexed, now)) return false;
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2129
2271
|
// src/cleanup-completion-blocker.ts
|
|
2130
2272
|
init_landing_gate();
|
|
2131
2273
|
init_status();
|
|
@@ -2147,51 +2289,80 @@ function completionBlockerBlocksWorktreeRemoval(indexed, status) {
|
|
|
2147
2289
|
return false;
|
|
2148
2290
|
}
|
|
2149
2291
|
|
|
2150
|
-
// src/cleanup-
|
|
2292
|
+
// src/cleanup-salvage-evidence.ts
|
|
2293
|
+
init_git();
|
|
2151
2294
|
init_util();
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
"
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2295
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2296
|
+
import path10 from "node:path";
|
|
2297
|
+
function salvageEvidenceDir(harnessRoot, runId, workerName) {
|
|
2298
|
+
return path10.join(harnessRoot, "salvage", safeSlug(runId), safeSlug(workerName));
|
|
2299
|
+
}
|
|
2300
|
+
function hasSalvageEvidence(input) {
|
|
2301
|
+
return existsSync10(path10.join(salvageEvidenceDir(input.harnessRoot, input.runId, input.workerName), "evidence.json"));
|
|
2302
|
+
}
|
|
2303
|
+
function writeWorktreeSalvageEvidence(input) {
|
|
2304
|
+
const dir = salvageEvidenceDir(input.harnessRoot, input.indexed.runId, input.indexed.workerName);
|
|
2305
|
+
mkdirSync4(dir, { recursive: true });
|
|
2306
|
+
const patchPath = path10.join(dir, "salvage.patch");
|
|
2307
|
+
let wrotePatch = false;
|
|
2308
|
+
if (existsSync10(input.indexed.worktreePath)) {
|
|
2309
|
+
const diff = gitCapture(input.indexed.worktreePath, ["diff", "HEAD"]);
|
|
2310
|
+
const staged = gitCapture(input.indexed.worktreePath, ["diff", "--cached"]);
|
|
2311
|
+
const patch = [diff.stdout, staged.stdout].filter((chunk) => chunk.trim()).join("\n");
|
|
2312
|
+
if (patch.trim()) {
|
|
2313
|
+
writeFileSync3(patchPath, patch.endsWith("\n") ? patch : `${patch}
|
|
2314
|
+
`);
|
|
2315
|
+
wrotePatch = true;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
const record = {
|
|
2319
|
+
capturedAt: new Date(input.now ?? Date.now()).toISOString(),
|
|
2320
|
+
skipReason: input.skipReason,
|
|
2321
|
+
runId: input.indexed.runId,
|
|
2322
|
+
workerName: input.indexed.workerName,
|
|
2323
|
+
worktreePath: input.indexed.worktreePath,
|
|
2324
|
+
taskId: input.indexed.worker.taskId,
|
|
2325
|
+
agentOsId: input.indexed.worker.agentOsId,
|
|
2326
|
+
branch: input.indexed.worker.branch,
|
|
2327
|
+
completionBlocker: input.indexed.worker.completionBlocker,
|
|
2328
|
+
finalResult: input.status.finalResult,
|
|
2329
|
+
changedFiles: input.status.changedFiles,
|
|
2330
|
+
gitAncestry: input.status.gitAncestry,
|
|
2331
|
+
prUrl: input.status.prUrl ?? null,
|
|
2332
|
+
...wrotePatch ? { patchPath } : {}
|
|
2333
|
+
};
|
|
2334
|
+
writeFileSync3(path10.join(dir, "evidence.json"), `${JSON.stringify(record, null, 2)}
|
|
2335
|
+
`);
|
|
2336
|
+
return record;
|
|
2192
2337
|
}
|
|
2193
2338
|
|
|
2194
2339
|
// src/cleanup-guards.ts
|
|
2340
|
+
var SALVAGE_ELIGIBLE_SKIP_REASONS = /* @__PURE__ */ new Set([
|
|
2341
|
+
"completion_blocked",
|
|
2342
|
+
"dirty_worktree",
|
|
2343
|
+
"pr_or_unmerged_commits",
|
|
2344
|
+
"landing_blocked"
|
|
2345
|
+
]);
|
|
2346
|
+
function maybeSalvageBlockedWorktree(input) {
|
|
2347
|
+
if (!SALVAGE_ELIGIBLE_SKIP_REASONS.has(input.skipReason)) return false;
|
|
2348
|
+
if (isWorkerProcessLive(input.indexed, input.now)) return false;
|
|
2349
|
+
if (hasSalvageEvidence({
|
|
2350
|
+
harnessRoot: input.harnessRoot,
|
|
2351
|
+
runId: input.indexed.runId,
|
|
2352
|
+
workerName: input.indexed.workerName
|
|
2353
|
+
})) {
|
|
2354
|
+
return true;
|
|
2355
|
+
}
|
|
2356
|
+
if (!input.writeSalvageEvidence) return false;
|
|
2357
|
+
writeWorktreeSalvageEvidence({
|
|
2358
|
+
harnessRoot: input.harnessRoot,
|
|
2359
|
+
indexed: input.indexed,
|
|
2360
|
+
skipReason: input.skipReason,
|
|
2361
|
+
status: input.status,
|
|
2362
|
+
now: input.now
|
|
2363
|
+
});
|
|
2364
|
+
return true;
|
|
2365
|
+
}
|
|
2195
2366
|
function effectiveWorktreeAgeMs(input) {
|
|
2196
2367
|
const { indexed, includeOrphans, worktreesAgeMs, terminalWorktreesAgeMs } = input;
|
|
2197
2368
|
if (!indexed) return includeOrphans ? terminalWorktreesAgeMs : worktreesAgeMs;
|
|
@@ -2206,8 +2377,18 @@ function effectiveWorktreeAgeMs(input) {
|
|
|
2206
2377
|
}
|
|
2207
2378
|
return worktreesAgeMs;
|
|
2208
2379
|
}
|
|
2380
|
+
function resolveHarnessRoot2(input, indexed) {
|
|
2381
|
+
if (input.harnessRoot?.trim()) return path11.resolve(input.harnessRoot);
|
|
2382
|
+
const workerDir = indexed.worker.workerDir;
|
|
2383
|
+
if (!workerDir) return null;
|
|
2384
|
+
const runsMarker = `${path11.sep}runs${path11.sep}`;
|
|
2385
|
+
const idx = workerDir.indexOf(runsMarker);
|
|
2386
|
+
if (idx < 0) return null;
|
|
2387
|
+
return workerDir.slice(0, idx + runsMarker.length - 1);
|
|
2388
|
+
}
|
|
2209
2389
|
function skipWorktreeRemoval(input) {
|
|
2210
2390
|
const { indexed, includeOrphans, worktreesAgeMs, ageMs, orphanSafety, worktreeRemovalGuard } = input;
|
|
2391
|
+
const now = input.now ?? Date.now();
|
|
2211
2392
|
if (!indexed) {
|
|
2212
2393
|
if (!includeOrphans) return "orphan_without_flag";
|
|
2213
2394
|
return orphanSafety ?? null;
|
|
@@ -2215,25 +2396,55 @@ function skipWorktreeRemoval(input) {
|
|
|
2215
2396
|
const ageThresholdMs = effectiveWorktreeAgeMs(input);
|
|
2216
2397
|
if (worktreesAgeMs <= 0 && !includeOrphans && ageThresholdMs <= 0) return "worktrees_disabled";
|
|
2217
2398
|
if (ageThresholdMs > 0 && ageMs < ageThresholdMs) return "below_age_threshold";
|
|
2218
|
-
if (isWorkerProcessLive(indexed)) return "active_worker";
|
|
2399
|
+
if (isWorkerProcessLive(indexed, now)) return "active_worker";
|
|
2400
|
+
const status = resolveWorktreeGuardStatus(indexed, input.liveness);
|
|
2401
|
+
const salvageHarnessRoot = resolveHarnessRoot2(input, indexed);
|
|
2402
|
+
const salvageOrBlock = (skipReason) => {
|
|
2403
|
+
if (salvageHarnessRoot && maybeSalvageBlockedWorktree({
|
|
2404
|
+
indexed,
|
|
2405
|
+
harnessRoot: salvageHarnessRoot,
|
|
2406
|
+
skipReason,
|
|
2407
|
+
status,
|
|
2408
|
+
now,
|
|
2409
|
+
writeSalvageEvidence: input.writeSalvageEvidence === true
|
|
2410
|
+
})) {
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
return skipReason;
|
|
2414
|
+
};
|
|
2415
|
+
if (completionBlockerBlocksWorktreeRemoval(indexed, status)) {
|
|
2416
|
+
const blocked = salvageOrBlock("completion_blocked");
|
|
2417
|
+
if (blocked) return blocked;
|
|
2418
|
+
}
|
|
2219
2419
|
if (indexedWorktreeHasMaterialChanges(indexed, input.liveness?.gitStatusCache)) {
|
|
2220
|
-
|
|
2420
|
+
const blocked = salvageOrBlock("dirty_worktree");
|
|
2421
|
+
if (blocked) return blocked;
|
|
2221
2422
|
}
|
|
2222
2423
|
const ahead = input.liveness?.gitRevCache?.countAheadOfMain(input.worktreePath);
|
|
2223
|
-
if (ahead !== null && ahead !== void 0 && ahead > 0)
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2424
|
+
if (ahead !== null && ahead !== void 0 && ahead > 0) {
|
|
2425
|
+
const blocked = salvageOrBlock("pr_or_unmerged_commits");
|
|
2426
|
+
if (blocked) return blocked;
|
|
2427
|
+
}
|
|
2428
|
+
if (runBlocksWorktreeRemoval(indexed, input.liveness, now)) return "run_still_active";
|
|
2227
2429
|
if (!isFinishedWorkerStatus(status)) return "run_still_active";
|
|
2228
|
-
if (isPrOrUnmergedWork(status))
|
|
2229
|
-
|
|
2430
|
+
if (isPrOrUnmergedWork(status)) {
|
|
2431
|
+
const blocked = salvageOrBlock("pr_or_unmerged_commits");
|
|
2432
|
+
if (blocked) return blocked;
|
|
2433
|
+
}
|
|
2434
|
+
if (materialWorktreeChanges(status.changedFiles).length > 0) {
|
|
2435
|
+
const blocked = salvageOrBlock("dirty_worktree");
|
|
2436
|
+
if (blocked) return blocked;
|
|
2437
|
+
}
|
|
2230
2438
|
const landing = assessWorkerLanding({
|
|
2231
2439
|
finalResult: status.finalResult,
|
|
2232
2440
|
changedFiles: status.changedFiles,
|
|
2233
2441
|
gitAncestry: status.gitAncestry,
|
|
2234
2442
|
prUrl: prUrlFromFinalResult(status.finalResult)
|
|
2235
2443
|
});
|
|
2236
|
-
if (landing.blocked)
|
|
2444
|
+
if (landing.blocked) {
|
|
2445
|
+
const blocked = salvageOrBlock("landing_blocked");
|
|
2446
|
+
if (blocked) return blocked;
|
|
2447
|
+
}
|
|
2237
2448
|
if (worktreeRemovalGuard && input.worktreePath) {
|
|
2238
2449
|
const overlay = worktreeRemovalGuard({
|
|
2239
2450
|
worktreePath: input.worktreePath,
|
|
@@ -2250,7 +2461,7 @@ function skipWorktreeRemoval(input) {
|
|
|
2250
2461
|
function skipDependencyCacheRemoval(input) {
|
|
2251
2462
|
const { indexed, nodeModulesAgeMs, ageMs, worktreePath, activeWorktreePaths, diskPressure } = input;
|
|
2252
2463
|
if (!diskPressure && ageMs < nodeModulesAgeMs) return "below_age_threshold";
|
|
2253
|
-
if (activeWorktreePaths.has(
|
|
2464
|
+
if (activeWorktreePaths.has(path11.resolve(worktreePath))) return "active_worker";
|
|
2254
2465
|
if (indexed && isWorkerProcessLive(indexed)) return "active_worker";
|
|
2255
2466
|
if (indexed && indexedWorktreeHasMaterialChanges(indexed, input.gitStatusCache)) {
|
|
2256
2467
|
return "dirty_worktree";
|
|
@@ -2279,11 +2490,11 @@ var LIVE_SKIP_REASONS = /* @__PURE__ */ new Set([
|
|
|
2279
2490
|
function collectPreservedLivePaths(actions, skips) {
|
|
2280
2491
|
const out = [];
|
|
2281
2492
|
const seen = /* @__PURE__ */ new Set();
|
|
2282
|
-
const push = (
|
|
2283
|
-
const key = `${
|
|
2493
|
+
const push = (path26, reason, detail) => {
|
|
2494
|
+
const key = `${path26}\0${reason}`;
|
|
2284
2495
|
if (seen.has(key) || out.length >= MAX_PRESERVED_LIVE_PATH_SAMPLES) return;
|
|
2285
2496
|
seen.add(key);
|
|
2286
|
-
out.push({ path:
|
|
2497
|
+
out.push({ path: path26, reason, ...detail ? { detail } : {} });
|
|
2287
2498
|
};
|
|
2288
2499
|
for (const skip of skips) {
|
|
2289
2500
|
if (!LIVE_SKIP_REASONS.has(skip.reason)) continue;
|
|
@@ -2298,134 +2509,16 @@ function collectPreservedLivePaths(actions, skips) {
|
|
|
2298
2509
|
}
|
|
2299
2510
|
|
|
2300
2511
|
// src/cleanup-run-directory.ts
|
|
2301
|
-
import { existsSync as existsSync11, readdirSync as
|
|
2302
|
-
import
|
|
2512
|
+
import { existsSync as existsSync11, readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
|
|
2513
|
+
import path13 from "node:path";
|
|
2303
2514
|
|
|
2304
2515
|
// src/cleanup-active-worktrees.ts
|
|
2305
2516
|
init_run_store();
|
|
2306
2517
|
init_paths();
|
|
2307
|
-
import
|
|
2308
|
-
import path11 from "node:path";
|
|
2309
|
-
|
|
2310
|
-
// src/run-metadata-retention.ts
|
|
2311
|
-
import { existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
2312
|
-
import path10 from "node:path";
|
|
2313
|
-
|
|
2314
|
-
// src/default-repo.ts
|
|
2315
|
-
init_config();
|
|
2316
|
-
init_default_repo_discovery();
|
|
2317
|
-
init_path_values();
|
|
2318
|
-
|
|
2319
|
-
// src/run-metadata-retention.ts
|
|
2320
|
-
init_heartbeat();
|
|
2321
|
-
init_paths();
|
|
2322
|
-
init_run_store();
|
|
2323
|
-
|
|
2324
|
-
// src/worker-metadata-paths.ts
|
|
2325
|
-
init_paths();
|
|
2326
|
-
init_util();
|
|
2327
|
-
import path9 from "node:path";
|
|
2328
|
-
var NESTED_RUNS = `${path9.sep}runs${path9.sep}runs${path9.sep}`;
|
|
2329
|
-
function workerArtifactPaths(workerDir) {
|
|
2330
|
-
return {
|
|
2331
|
-
workerJsonPath: path9.join(workerDir, "worker.json"),
|
|
2332
|
-
stdoutPath: path9.join(workerDir, "stdout.jsonl"),
|
|
2333
|
-
stderrPath: path9.join(workerDir, "stderr.log"),
|
|
2334
|
-
heartbeatPath: path9.join(workerDir, "heartbeat.jsonl"),
|
|
2335
|
-
lastStatusPath: path9.join(workerDir, "last-status.json")
|
|
2336
|
-
};
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
// src/run-metadata-retention.ts
|
|
2340
|
-
init_util();
|
|
2341
|
-
var RUN_METADATA_ACTIVE_SIGNAL_MS = 15 * 60 * 1e3;
|
|
2342
|
-
function isHarnessRunMetadataPath(targetPath, harnessRoot) {
|
|
2343
|
-
const resolved = path10.resolve(targetPath);
|
|
2344
|
-
const runsDir = path10.resolve(harnessRunsDir(harnessRoot));
|
|
2345
|
-
const rel = path10.relative(runsDir, resolved);
|
|
2346
|
-
return rel !== ".." && !rel.startsWith("..") && !path10.isAbsolute(rel);
|
|
2347
|
-
}
|
|
2348
|
-
function listRunDirIds(runsDir) {
|
|
2349
|
-
if (!existsSync9(runsDir)) return [];
|
|
2350
|
-
try {
|
|
2351
|
-
return readdirSync4(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== "runs").map((entry) => entry.name);
|
|
2352
|
-
} catch {
|
|
2353
|
-
return [];
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
function listWorkerNamesOnDisk(runDir2) {
|
|
2357
|
-
const workersDir = path10.join(runDir2, "workers");
|
|
2358
|
-
if (!existsSync9(workersDir)) return [];
|
|
2359
|
-
try {
|
|
2360
|
-
return readdirSync4(workersDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2361
|
-
} catch {
|
|
2362
|
-
return [];
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
function pathRecentlyTouched(target, now, windowMs) {
|
|
2366
|
-
if (!existsSync9(target)) return false;
|
|
2367
|
-
try {
|
|
2368
|
-
const age = now - statSync3(target).mtimeMs;
|
|
2369
|
-
return Number.isFinite(age) && age >= 0 && age < windowMs;
|
|
2370
|
-
} catch {
|
|
2371
|
-
return false;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
function workerDirHasActiveRetentionSignals(workerDir, now = Date.now(), windowMs = RUN_METADATA_ACTIVE_SIGNAL_MS) {
|
|
2375
|
-
if (!existsSync9(workerDir)) return false;
|
|
2376
|
-
const artifacts = workerArtifactPaths(workerDir);
|
|
2377
|
-
const worker = readJson(
|
|
2378
|
-
artifacts.workerJsonPath,
|
|
2379
|
-
void 0
|
|
2380
|
-
);
|
|
2381
|
-
if (worker?.status === "running" && isPidAlive(worker.pid)) return true;
|
|
2382
|
-
const heartbeat = parseHeartbeat(artifacts.heartbeatPath);
|
|
2383
|
-
if (heartbeat.lastHeartbeatAt) {
|
|
2384
|
-
const age = now - Date.parse(heartbeat.lastHeartbeatAt);
|
|
2385
|
-
if (Number.isFinite(age) && age >= 0 && age < windowMs) return true;
|
|
2386
|
-
}
|
|
2387
|
-
if (pathRecentlyTouched(artifacts.stdoutPath, now, windowMs)) return true;
|
|
2388
|
-
if (pathRecentlyTouched(artifacts.heartbeatPath, now, windowMs)) return true;
|
|
2389
|
-
return false;
|
|
2390
|
-
}
|
|
2391
|
-
function runDirHasActiveRetentionSignals(runDir2, now = Date.now(), windowMs = RUN_METADATA_ACTIVE_SIGNAL_MS) {
|
|
2392
|
-
for (const name of listWorkerNamesOnDisk(runDir2)) {
|
|
2393
|
-
if (workerDirHasActiveRetentionSignals(path10.join(runDir2, "workers", name), now, windowMs)) {
|
|
2394
|
-
return true;
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
return false;
|
|
2398
|
-
}
|
|
2399
|
-
function collectFilesystemLiveRunKeys(harnessRoot, now = Date.now()) {
|
|
2400
|
-
const keys = /* @__PURE__ */ new Set();
|
|
2401
|
-
const runsDir = harnessRunsDir(harnessRoot);
|
|
2402
|
-
for (const runId of listRunDirIds(runsDir)) {
|
|
2403
|
-
const runDir2 = path10.join(runsDir, runId);
|
|
2404
|
-
if (runDirHasActiveRetentionSignals(runDir2, now)) {
|
|
2405
|
-
keys.add(`${harnessRoot}\0${runId}`);
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
return keys;
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
// src/cleanup-active-worktrees.ts
|
|
2518
|
+
import path12 from "node:path";
|
|
2412
2519
|
init_util();
|
|
2413
|
-
function workerHasRecentHarnessActivity(worker, now) {
|
|
2414
|
-
const paths = [worker.heartbeatPath, worker.stdoutPath, worker.stderrPath];
|
|
2415
|
-
for (const target of paths) {
|
|
2416
|
-
if (!existsSync10(target)) continue;
|
|
2417
|
-
try {
|
|
2418
|
-
const age = now - statSync4(target).mtimeMs;
|
|
2419
|
-
if (Number.isFinite(age) && age >= 0 && age < RUN_METADATA_ACTIVE_SIGNAL_MS) return true;
|
|
2420
|
-
} catch {
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
return false;
|
|
2424
|
-
}
|
|
2425
2520
|
function isActiveHarnessWorker2(worker, now) {
|
|
2426
|
-
|
|
2427
|
-
if (worker.status === "running" && workerHasRecentHarnessActivity(worker, now)) return true;
|
|
2428
|
-
return false;
|
|
2521
|
+
return isHarnessWorkerHarnessLive(worker, now);
|
|
2429
2522
|
}
|
|
2430
2523
|
function collectActiveWorktreeGuards(harnessRoots, now = Date.now()) {
|
|
2431
2524
|
const activeWorktreePaths = /* @__PURE__ */ new Set();
|
|
@@ -2435,11 +2528,11 @@ function collectActiveWorktreeGuards(harnessRoots, now = Date.now()) {
|
|
|
2435
2528
|
let runHasLive = false;
|
|
2436
2529
|
for (const name of Object.keys(run.workers || {})) {
|
|
2437
2530
|
const worker = readJson(
|
|
2438
|
-
|
|
2531
|
+
path12.join(runDirectoryAt(harnessRoot, run.id), "workers", safeSlug(name), "worker.json"),
|
|
2439
2532
|
void 0
|
|
2440
2533
|
);
|
|
2441
2534
|
if (!worker?.worktreePath) continue;
|
|
2442
|
-
const worktreePath =
|
|
2535
|
+
const worktreePath = path12.resolve(worker.worktreePath);
|
|
2443
2536
|
if (!isActiveHarnessWorker2(worker, now)) continue;
|
|
2444
2537
|
runHasLive = true;
|
|
2445
2538
|
activeWorktreePaths.add(worktreePath);
|
|
@@ -2461,20 +2554,20 @@ function isWorktreeOnLiveRun(worktreePath, harnessRoot, runId, liveRunKeys) {
|
|
|
2461
2554
|
init_util();
|
|
2462
2555
|
function pathAgeMs(target, now) {
|
|
2463
2556
|
try {
|
|
2464
|
-
const mtime =
|
|
2557
|
+
const mtime = statSync4(target).mtimeMs;
|
|
2465
2558
|
return Math.max(0, now - mtime);
|
|
2466
2559
|
} catch {
|
|
2467
2560
|
return 0;
|
|
2468
2561
|
}
|
|
2469
2562
|
}
|
|
2470
2563
|
function loadRunStatus(harnessRoot, runId) {
|
|
2471
|
-
const runPath =
|
|
2564
|
+
const runPath = path13.join(harnessRoot, "runs", runId, "run.json");
|
|
2472
2565
|
if (!existsSync11(runPath)) return null;
|
|
2473
2566
|
return readJson(runPath, null);
|
|
2474
2567
|
}
|
|
2475
2568
|
function runDirectoryIsEmpty(runPath) {
|
|
2476
2569
|
try {
|
|
2477
|
-
const entries =
|
|
2570
|
+
const entries = readdirSync5(runPath);
|
|
2478
2571
|
return entries.length === 0;
|
|
2479
2572
|
} catch {
|
|
2480
2573
|
return false;
|
|
@@ -2498,7 +2591,7 @@ function scanStaleRunDirectoryCandidates(opts) {
|
|
|
2498
2591
|
const candidates = [];
|
|
2499
2592
|
let entries;
|
|
2500
2593
|
try {
|
|
2501
|
-
entries =
|
|
2594
|
+
entries = readdirSync5(opts.worktreesDir, { withFileTypes: true });
|
|
2502
2595
|
} catch {
|
|
2503
2596
|
return [];
|
|
2504
2597
|
}
|
|
@@ -2506,7 +2599,7 @@ function scanStaleRunDirectoryCandidates(opts) {
|
|
|
2506
2599
|
if (!runEntry.isDirectory()) continue;
|
|
2507
2600
|
const runId = runEntry.name;
|
|
2508
2601
|
if (opts.runIdFilter && runId !== opts.runIdFilter) continue;
|
|
2509
|
-
const runPath =
|
|
2602
|
+
const runPath = path13.join(opts.worktreesDir, runId);
|
|
2510
2603
|
if (!runDirectoryIsEmpty(runPath)) continue;
|
|
2511
2604
|
candidates.push({
|
|
2512
2605
|
kind: "remove_run_directory",
|
|
@@ -2526,8 +2619,8 @@ import { existsSync as existsSync14, rmSync as rmSync2 } from "node:fs";
|
|
|
2526
2619
|
|
|
2527
2620
|
// src/cleanup-dir-size.ts
|
|
2528
2621
|
import { execFileSync } from "node:child_process";
|
|
2529
|
-
import { existsSync as existsSync12, readdirSync as
|
|
2530
|
-
import
|
|
2622
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, statSync as statSync5 } from "node:fs";
|
|
2623
|
+
import path14 from "node:path";
|
|
2531
2624
|
var DEFAULT_DU_TIMEOUT_MS = 2500;
|
|
2532
2625
|
function directorySizeBytesDu(root, timeoutMs = DEFAULT_DU_TIMEOUT_MS) {
|
|
2533
2626
|
if (!existsSync12(root)) return 0;
|
|
@@ -2555,16 +2648,16 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
2555
2648
|
const current = stack.pop();
|
|
2556
2649
|
let entries;
|
|
2557
2650
|
try {
|
|
2558
|
-
entries =
|
|
2651
|
+
entries = readdirSync6(current);
|
|
2559
2652
|
} catch {
|
|
2560
2653
|
continue;
|
|
2561
2654
|
}
|
|
2562
2655
|
for (const name of entries) {
|
|
2563
2656
|
if (seen++ > maxEntries) return null;
|
|
2564
|
-
const full =
|
|
2657
|
+
const full = path14.join(current, name);
|
|
2565
2658
|
let st;
|
|
2566
2659
|
try {
|
|
2567
|
-
st =
|
|
2660
|
+
st = statSync5(full);
|
|
2568
2661
|
} catch {
|
|
2569
2662
|
continue;
|
|
2570
2663
|
}
|
|
@@ -2580,7 +2673,7 @@ import { existsSync as existsSync13, rmSync } from "node:fs";
|
|
|
2580
2673
|
init_paths();
|
|
2581
2674
|
|
|
2582
2675
|
// src/cleanup-path-ownership.ts
|
|
2583
|
-
import { lstatSync, readdirSync as
|
|
2676
|
+
import { lstatSync, readdirSync as readdirSync7 } from "node:fs";
|
|
2584
2677
|
function readPathOwnership(targetPath) {
|
|
2585
2678
|
try {
|
|
2586
2679
|
const st = lstatSync(targetPath);
|
|
@@ -2597,7 +2690,7 @@ function pathHasForeignOwnedEntry(targetPath, maxEntries = 32) {
|
|
|
2597
2690
|
if (!root) return false;
|
|
2598
2691
|
if (root.foreign) return true;
|
|
2599
2692
|
try {
|
|
2600
|
-
const names =
|
|
2693
|
+
const names = readdirSync7(targetPath);
|
|
2601
2694
|
let checked = 0;
|
|
2602
2695
|
for (const name of names) {
|
|
2603
2696
|
if (checked >= maxEntries) break;
|
|
@@ -2613,20 +2706,20 @@ function pathHasForeignOwnedEntry(targetPath, maxEntries = 32) {
|
|
|
2613
2706
|
|
|
2614
2707
|
// src/cleanup-privileged-remove.ts
|
|
2615
2708
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2616
|
-
import
|
|
2709
|
+
import path16 from "node:path";
|
|
2617
2710
|
|
|
2618
2711
|
// src/cleanup-harness-path-validate.ts
|
|
2619
|
-
import
|
|
2712
|
+
import path15 from "node:path";
|
|
2620
2713
|
function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
|
|
2621
|
-
const resolved =
|
|
2622
|
-
const suffix = `${
|
|
2714
|
+
const resolved = path15.resolve(targetPath);
|
|
2715
|
+
const suffix = `${path15.sep}${cacheDirName}`;
|
|
2623
2716
|
const cachePath = resolved.endsWith(suffix) ? resolved : null;
|
|
2624
2717
|
if (!cachePath) return "path_outside_harness";
|
|
2625
|
-
const rel =
|
|
2626
|
-
if (rel.startsWith("..") ||
|
|
2627
|
-
const parts = rel.split(
|
|
2718
|
+
const rel = path15.relative(worktreesDir, cachePath);
|
|
2719
|
+
if (rel.startsWith("..") || path15.isAbsolute(rel)) return "path_outside_harness";
|
|
2720
|
+
const parts = rel.split(path15.sep);
|
|
2628
2721
|
if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
|
|
2629
|
-
if (!resolved.startsWith(
|
|
2722
|
+
if (!resolved.startsWith(path15.resolve(harnessRoot))) return "path_outside_harness";
|
|
2630
2723
|
return null;
|
|
2631
2724
|
}
|
|
2632
2725
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
@@ -2636,16 +2729,16 @@ function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
|
|
|
2636
2729
|
return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
|
|
2637
2730
|
}
|
|
2638
2731
|
function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
|
|
2639
|
-
const resolved =
|
|
2640
|
-
const relToWt =
|
|
2641
|
-
if (relToWt.startsWith("..") ||
|
|
2642
|
-
const parts = relToWt.split(
|
|
2732
|
+
const resolved = path15.resolve(targetPath);
|
|
2733
|
+
const relToWt = path15.relative(worktreesDir, resolved);
|
|
2734
|
+
if (relToWt.startsWith("..") || path15.isAbsolute(relToWt)) return "path_outside_harness";
|
|
2735
|
+
const parts = relToWt.split(path15.sep);
|
|
2643
2736
|
if (parts.length < 3) return "path_outside_harness";
|
|
2644
|
-
if (!resolved.startsWith(
|
|
2737
|
+
if (!resolved.startsWith(path15.resolve(harnessRoot))) return "path_outside_harness";
|
|
2645
2738
|
return null;
|
|
2646
2739
|
}
|
|
2647
2740
|
function isHarnessGeneratedCachePath(targetPath, harnessRoot, worktreesDir) {
|
|
2648
|
-
const resolved =
|
|
2741
|
+
const resolved = path15.resolve(targetPath);
|
|
2649
2742
|
return isHarnessNodeModulesPath(resolved, harnessRoot, worktreesDir) === null || isHarnessNextCachePath(resolved, harnessRoot, worktreesDir) === null || isHarnessBuildCachePath(resolved, harnessRoot, worktreesDir) === null;
|
|
2650
2743
|
}
|
|
2651
2744
|
|
|
@@ -2679,12 +2772,12 @@ function tryPrivilegedReclaimHarnessCache(targetPath, harnessRoot, worktreesDir)
|
|
|
2679
2772
|
"chown",
|
|
2680
2773
|
"-R",
|
|
2681
2774
|
`${effectiveUid}:${effectiveGid}`,
|
|
2682
|
-
|
|
2775
|
+
path16.resolve(targetPath)
|
|
2683
2776
|
]);
|
|
2684
2777
|
if (chown.ok) {
|
|
2685
2778
|
return { ok: true, method: "chown_then_rm" };
|
|
2686
2779
|
}
|
|
2687
|
-
const rm = runSudoNonInteractive(["rm", "-rf",
|
|
2780
|
+
const rm = runSudoNonInteractive(["rm", "-rf", path16.resolve(targetPath)]);
|
|
2688
2781
|
if (rm.ok) {
|
|
2689
2782
|
return { ok: true, method: "sudo_rm" };
|
|
2690
2783
|
}
|
|
@@ -2885,27 +2978,27 @@ function removeWorktree(candidate, execute) {
|
|
|
2885
2978
|
}
|
|
2886
2979
|
|
|
2887
2980
|
// src/cleanup-scan.ts
|
|
2888
|
-
import { existsSync as existsSync15, readdirSync as
|
|
2889
|
-
import
|
|
2981
|
+
import { existsSync as existsSync15, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
|
|
2982
|
+
import path17 from "node:path";
|
|
2890
2983
|
function pathAgeMs2(target, now) {
|
|
2891
2984
|
try {
|
|
2892
|
-
const mtime =
|
|
2985
|
+
const mtime = statSync6(target).mtimeMs;
|
|
2893
2986
|
return Math.max(0, now - mtime);
|
|
2894
2987
|
} catch {
|
|
2895
2988
|
return 0;
|
|
2896
2989
|
}
|
|
2897
2990
|
}
|
|
2898
2991
|
function isPathInside(child, parent) {
|
|
2899
|
-
const rel =
|
|
2900
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
2992
|
+
const rel = path17.relative(parent, child);
|
|
2993
|
+
return rel === "" || !rel.startsWith("..") && !path17.isAbsolute(rel);
|
|
2901
2994
|
}
|
|
2902
2995
|
function collectBuildCacheForWorktree(worktreePath, opts, seen, meta) {
|
|
2903
2996
|
const out = [];
|
|
2904
2997
|
for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
|
|
2905
2998
|
if (rel === ".next") continue;
|
|
2906
|
-
const target =
|
|
2999
|
+
const target = path17.join(worktreePath, rel);
|
|
2907
3000
|
if (!existsSync15(target)) continue;
|
|
2908
|
-
const resolved =
|
|
3001
|
+
const resolved = path17.resolve(target);
|
|
2909
3002
|
if (seen.has(resolved)) continue;
|
|
2910
3003
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
2911
3004
|
seen.add(resolved);
|
|
@@ -2935,12 +3028,12 @@ function scanBuildCacheCandidates(opts) {
|
|
|
2935
3028
|
);
|
|
2936
3029
|
}
|
|
2937
3030
|
if (!opts.includeOrphans || !existsSync15(opts.worktreesDir)) return candidates;
|
|
2938
|
-
for (const runEntry of
|
|
3031
|
+
for (const runEntry of readdirSync8(opts.worktreesDir, { withFileTypes: true })) {
|
|
2939
3032
|
if (!runEntry.isDirectory()) continue;
|
|
2940
|
-
const runPath =
|
|
2941
|
-
for (const workerEntry of
|
|
3033
|
+
const runPath = path17.join(opts.worktreesDir, runEntry.name);
|
|
3034
|
+
for (const workerEntry of readdirSync8(runPath, { withFileTypes: true })) {
|
|
2942
3035
|
if (!workerEntry.isDirectory()) continue;
|
|
2943
|
-
const worktreePath =
|
|
3036
|
+
const worktreePath = path17.join(runPath, workerEntry.name);
|
|
2944
3037
|
candidates.push(
|
|
2945
3038
|
...collectBuildCacheForWorktree(worktreePath, opts, seen, {
|
|
2946
3039
|
runId: runEntry.name,
|
|
@@ -2978,21 +3071,21 @@ function scanWorktreeCandidates(opts) {
|
|
|
2978
3071
|
if (!orphanEnabled || !existsSync15(opts.worktreesDir)) return candidates;
|
|
2979
3072
|
const indexedPaths = /* @__PURE__ */ new Set();
|
|
2980
3073
|
for (const entry of opts.index.values()) {
|
|
2981
|
-
indexedPaths.add(
|
|
3074
|
+
indexedPaths.add(path17.resolve(entry.worktreePath));
|
|
2982
3075
|
}
|
|
2983
|
-
for (const runEntry of
|
|
3076
|
+
for (const runEntry of readdirSync8(opts.worktreesDir, { withFileTypes: true })) {
|
|
2984
3077
|
if (!runEntry.isDirectory()) continue;
|
|
2985
3078
|
if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
|
|
2986
|
-
const runPath =
|
|
3079
|
+
const runPath = path17.join(opts.worktreesDir, runEntry.name);
|
|
2987
3080
|
let workerEntries;
|
|
2988
3081
|
try {
|
|
2989
|
-
workerEntries =
|
|
3082
|
+
workerEntries = readdirSync8(runPath, { withFileTypes: true });
|
|
2990
3083
|
} catch {
|
|
2991
3084
|
continue;
|
|
2992
3085
|
}
|
|
2993
3086
|
for (const workerEntry of workerEntries) {
|
|
2994
3087
|
if (!workerEntry.isDirectory()) continue;
|
|
2995
|
-
const worktreePath =
|
|
3088
|
+
const worktreePath = path17.resolve(path17.join(runPath, workerEntry.name));
|
|
2996
3089
|
if (seen.has(worktreePath)) continue;
|
|
2997
3090
|
if (indexedPaths.has(worktreePath)) continue;
|
|
2998
3091
|
if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
|
|
@@ -3011,27 +3104,27 @@ function scanWorktreeCandidates(opts) {
|
|
|
3011
3104
|
}
|
|
3012
3105
|
|
|
3013
3106
|
// src/cleanup-dependency-scan.ts
|
|
3014
|
-
import { existsSync as existsSync16, readdirSync as
|
|
3015
|
-
import
|
|
3107
|
+
import { existsSync as existsSync16, readdirSync as readdirSync9, statSync as statSync7 } from "node:fs";
|
|
3108
|
+
import path18 from "node:path";
|
|
3016
3109
|
var DEPENDENCY_CACHE_DIRS = [
|
|
3017
3110
|
{ dirName: "node_modules", kind: "remove_node_modules" },
|
|
3018
3111
|
{ dirName: ".next", kind: "remove_next_cache" }
|
|
3019
3112
|
];
|
|
3020
3113
|
function pathAgeMs3(target, now) {
|
|
3021
3114
|
try {
|
|
3022
|
-
const mtime =
|
|
3115
|
+
const mtime = statSync7(target).mtimeMs;
|
|
3023
3116
|
return Math.max(0, now - mtime);
|
|
3024
3117
|
} catch {
|
|
3025
3118
|
return 0;
|
|
3026
3119
|
}
|
|
3027
3120
|
}
|
|
3028
3121
|
function isPathInside2(child, parent) {
|
|
3029
|
-
const rel =
|
|
3030
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
3122
|
+
const rel = path18.relative(parent, child);
|
|
3123
|
+
return rel === "" || !rel.startsWith("..") && !path18.isAbsolute(rel);
|
|
3031
3124
|
}
|
|
3032
3125
|
function pushCandidate(candidates, seen, opts, targetPath, kind, meta) {
|
|
3033
3126
|
if (!existsSync16(targetPath)) return;
|
|
3034
|
-
const resolved =
|
|
3127
|
+
const resolved = path18.resolve(targetPath);
|
|
3035
3128
|
if (seen.has(resolved)) return;
|
|
3036
3129
|
if (!isPathInside2(resolved, opts.harnessRoot)) return;
|
|
3037
3130
|
seen.add(resolved);
|
|
@@ -3048,7 +3141,7 @@ function pushCandidate(candidates, seen, opts, targetPath, kind, meta) {
|
|
|
3048
3141
|
}
|
|
3049
3142
|
function scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, meta) {
|
|
3050
3143
|
for (const entry of DEPENDENCY_CACHE_DIRS) {
|
|
3051
|
-
pushCandidate(candidates, seen, opts,
|
|
3144
|
+
pushCandidate(candidates, seen, opts, path18.join(worktreePath, entry.dirName), entry.kind, meta);
|
|
3052
3145
|
}
|
|
3053
3146
|
}
|
|
3054
3147
|
function scanDependencyCacheCandidates(opts) {
|
|
@@ -3063,19 +3156,19 @@ function scanDependencyCacheCandidates(opts) {
|
|
|
3063
3156
|
});
|
|
3064
3157
|
}
|
|
3065
3158
|
if (!opts.includeOrphans || !existsSync16(opts.worktreesDir)) return candidates;
|
|
3066
|
-
for (const runEntry of
|
|
3159
|
+
for (const runEntry of readdirSync9(opts.worktreesDir, { withFileTypes: true })) {
|
|
3067
3160
|
if (!runEntry.isDirectory()) continue;
|
|
3068
3161
|
if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
|
|
3069
|
-
const runPath =
|
|
3162
|
+
const runPath = path18.join(opts.worktreesDir, runEntry.name);
|
|
3070
3163
|
let workerEntries;
|
|
3071
3164
|
try {
|
|
3072
|
-
workerEntries =
|
|
3165
|
+
workerEntries = readdirSync9(runPath, { withFileTypes: true });
|
|
3073
3166
|
} catch {
|
|
3074
3167
|
continue;
|
|
3075
3168
|
}
|
|
3076
3169
|
for (const workerEntry of workerEntries) {
|
|
3077
3170
|
if (!workerEntry.isDirectory()) continue;
|
|
3078
|
-
const worktreePath =
|
|
3171
|
+
const worktreePath = path18.join(runPath, workerEntry.name);
|
|
3079
3172
|
scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, {
|
|
3080
3173
|
runId: runEntry.name,
|
|
3081
3174
|
worker: workerEntry.name
|
|
@@ -3087,11 +3180,11 @@ function scanDependencyCacheCandidates(opts) {
|
|
|
3087
3180
|
|
|
3088
3181
|
// src/cleanup-duplicate-worktrees.ts
|
|
3089
3182
|
init_git();
|
|
3090
|
-
import { existsSync as existsSync17, statSync as
|
|
3091
|
-
import
|
|
3183
|
+
import { existsSync as existsSync17, statSync as statSync8 } from "node:fs";
|
|
3184
|
+
import path19 from "node:path";
|
|
3092
3185
|
function pathAgeMs4(target, now) {
|
|
3093
3186
|
try {
|
|
3094
|
-
const mtime =
|
|
3187
|
+
const mtime = statSync8(target).mtimeMs;
|
|
3095
3188
|
return Math.max(0, now - mtime);
|
|
3096
3189
|
} catch {
|
|
3097
3190
|
return 0;
|
|
@@ -3118,32 +3211,24 @@ function parseWorktreePorcelain(output) {
|
|
|
3118
3211
|
return records;
|
|
3119
3212
|
}
|
|
3120
3213
|
function isUnderWorktreesDir(worktreePath, worktreesDir) {
|
|
3121
|
-
const rel =
|
|
3122
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
3123
|
-
}
|
|
3124
|
-
function isCleanWorktree(worktreePath, repoRoot) {
|
|
3125
|
-
try {
|
|
3126
|
-
const porcelain = git(repoRoot, ["-C", worktreePath, "status", "--porcelain"], {
|
|
3127
|
-
allowFailure: true
|
|
3128
|
-
});
|
|
3129
|
-
return !String(porcelain || "").trim();
|
|
3130
|
-
} catch {
|
|
3131
|
-
return false;
|
|
3132
|
-
}
|
|
3214
|
+
const rel = path19.relative(path19.resolve(worktreesDir), path19.resolve(worktreePath));
|
|
3215
|
+
return rel !== "" && !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
3133
3216
|
}
|
|
3217
|
+
var MAX_DUPLICATE_WORKTREE_CANDIDATES_PER_REPO = 200;
|
|
3134
3218
|
function scanDuplicateWorktreeCandidates(opts) {
|
|
3135
3219
|
if (!opts.includeOrphans || !existsSync17(opts.worktreesDir)) return [];
|
|
3136
3220
|
const repos = /* @__PURE__ */ new Set();
|
|
3137
3221
|
for (const entry of opts.index.values()) {
|
|
3138
|
-
if (entry.run.repo) repos.add(
|
|
3222
|
+
if (entry.run.repo) repos.add(path19.resolve(entry.run.repo));
|
|
3139
3223
|
}
|
|
3140
3224
|
const indexedPaths = /* @__PURE__ */ new Set();
|
|
3141
3225
|
for (const entry of opts.index.values()) {
|
|
3142
|
-
indexedPaths.add(
|
|
3226
|
+
indexedPaths.add(path19.resolve(entry.worktreePath));
|
|
3143
3227
|
}
|
|
3144
3228
|
const candidates = [];
|
|
3145
3229
|
const seen = /* @__PURE__ */ new Set();
|
|
3146
3230
|
for (const repoRoot of repos) {
|
|
3231
|
+
let repoCandidateCount = 0;
|
|
3147
3232
|
let porcelain;
|
|
3148
3233
|
try {
|
|
3149
3234
|
porcelain = git(repoRoot, ["worktree", "list", "--porcelain"], { allowFailure: true });
|
|
@@ -3152,18 +3237,19 @@ function scanDuplicateWorktreeCandidates(opts) {
|
|
|
3152
3237
|
}
|
|
3153
3238
|
const worktrees = parseWorktreePorcelain(porcelain);
|
|
3154
3239
|
for (const wt of worktrees) {
|
|
3155
|
-
|
|
3156
|
-
|
|
3240
|
+
if (repoCandidateCount >= MAX_DUPLICATE_WORKTREE_CANDIDATES_PER_REPO) break;
|
|
3241
|
+
const resolved = path19.resolve(wt.path);
|
|
3242
|
+
if (resolved === path19.resolve(repoRoot)) continue;
|
|
3157
3243
|
if (!isUnderWorktreesDir(resolved, opts.worktreesDir)) continue;
|
|
3158
3244
|
if (indexedPaths.has(resolved)) continue;
|
|
3159
3245
|
if (seen.has(resolved)) continue;
|
|
3160
3246
|
if (!existsSync17(resolved)) continue;
|
|
3161
|
-
|
|
3162
|
-
const
|
|
3163
|
-
const parts = rel.split(path18.sep);
|
|
3247
|
+
const rel = path19.relative(opts.worktreesDir, resolved);
|
|
3248
|
+
const parts = rel.split(path19.sep);
|
|
3164
3249
|
const runId = parts[0];
|
|
3165
3250
|
const worker = parts[1] ?? "unknown";
|
|
3166
3251
|
seen.add(resolved);
|
|
3252
|
+
repoCandidateCount += 1;
|
|
3167
3253
|
candidates.push({
|
|
3168
3254
|
kind: "remove_worktree",
|
|
3169
3255
|
path: resolved,
|
|
@@ -3181,12 +3267,22 @@ function scanDuplicateWorktreeCandidates(opts) {
|
|
|
3181
3267
|
// src/cleanup-worktree-index.ts
|
|
3182
3268
|
init_run_store();
|
|
3183
3269
|
init_util();
|
|
3184
|
-
import
|
|
3270
|
+
import path20 from "node:path";
|
|
3271
|
+
function filterWorktreeIndexForRoot(index, harnessRoot) {
|
|
3272
|
+
const resolvedRoot = path20.resolve(harnessRoot);
|
|
3273
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
3274
|
+
for (const [key, entry] of index) {
|
|
3275
|
+
const entryRoot = entry.harnessRoot ? path20.resolve(entry.harnessRoot) : null;
|
|
3276
|
+
if (entryRoot && entryRoot !== resolvedRoot) continue;
|
|
3277
|
+
scoped.set(key, entry);
|
|
3278
|
+
}
|
|
3279
|
+
return scoped;
|
|
3280
|
+
}
|
|
3185
3281
|
function buildWorktreeIndexAt(harnessRoot) {
|
|
3186
3282
|
const index = /* @__PURE__ */ new Map();
|
|
3187
3283
|
for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
|
|
3188
3284
|
for (const name of Object.keys(run.workers || {})) {
|
|
3189
|
-
const workerPath =
|
|
3285
|
+
const workerPath = path20.join(
|
|
3190
3286
|
runDirectoryAt(harnessRoot, run.id),
|
|
3191
3287
|
"workers",
|
|
3192
3288
|
safeSlug(name),
|
|
@@ -3194,9 +3290,9 @@ function buildWorktreeIndexAt(harnessRoot) {
|
|
|
3194
3290
|
);
|
|
3195
3291
|
const worker = readJson(workerPath, void 0);
|
|
3196
3292
|
if (!worker?.worktreePath) continue;
|
|
3197
|
-
index.set(
|
|
3293
|
+
index.set(path20.resolve(worker.worktreePath), {
|
|
3198
3294
|
harnessRoot,
|
|
3199
|
-
worktreePath:
|
|
3295
|
+
worktreePath: path20.resolve(worker.worktreePath),
|
|
3200
3296
|
runId: run.id,
|
|
3201
3297
|
workerName: name,
|
|
3202
3298
|
run,
|
|
@@ -3222,6 +3318,7 @@ function resolveHarnessRetention(options = {}) {
|
|
|
3222
3318
|
const execute = options.execute === true || options.execute !== false && envFlag("KYNVER_CLEANUP_EXECUTE");
|
|
3223
3319
|
const finalizeStaleRuns2 = options.finalizeStaleRuns !== false && !envFlag("KYNVER_CLEANUP_SKIP_FINALIZE");
|
|
3224
3320
|
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? envMs("KYNVER_CLEANUP_NODE_MODULES_AGE_MS", DEFAULT_NODE_MODULES_AGE_MS);
|
|
3321
|
+
const scanDependencyCaches = options.scanDependencyCaches ?? (options.nodeModulesAgeMs !== void 0 || process.env.KYNVER_CLEANUP_NODE_MODULES_AGE_MS != null);
|
|
3225
3322
|
const worktreesAgeMs = options.worktreesAgeMs ?? envMs("KYNVER_CLEANUP_WORKTREES_AGE_MS", 0);
|
|
3226
3323
|
const terminalWorktreesAgeMs = options.terminalWorktreesAgeMs ?? envMs("KYNVER_CLEANUP_TERMINAL_WORKTREES_AGE_MS", DEFAULT_TERMINAL_WORKTREES_AGE_MS);
|
|
3227
3324
|
const runDirectoriesAgeMs = options.runDirectoriesAgeMs ?? envMs("KYNVER_CLEANUP_RUN_DIRECTORIES_AGE_MS", DEFAULT_RUN_DIRECTORIES_AGE_MS);
|
|
@@ -3237,6 +3334,7 @@ function resolveHarnessRetention(options = {}) {
|
|
|
3237
3334
|
return {
|
|
3238
3335
|
execute,
|
|
3239
3336
|
finalizeStaleRuns: finalizeStaleRuns2,
|
|
3337
|
+
scanDependencyCaches,
|
|
3240
3338
|
nodeModulesAgeMs,
|
|
3241
3339
|
worktreesAgeMs: worktreesAgeMs > 0 ? worktreesAgeMs : 0,
|
|
3242
3340
|
terminalWorktreesAgeMs: terminalWorktreesAgeMs >= 0 ? terminalWorktreesAgeMs : 0,
|
|
@@ -3252,29 +3350,31 @@ function resolveHarnessRetention(options = {}) {
|
|
|
3252
3350
|
|
|
3253
3351
|
// src/cleanup-orphan-safety.ts
|
|
3254
3352
|
init_git();
|
|
3255
|
-
import { existsSync as existsSync18
|
|
3256
|
-
import
|
|
3353
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
3354
|
+
import path21 from "node:path";
|
|
3355
|
+
init_util();
|
|
3257
3356
|
var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
|
|
3258
3357
|
function assessOrphanWorktreeSafety(input) {
|
|
3259
3358
|
const now = input.now ?? Date.now();
|
|
3260
3359
|
const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
|
|
3261
3360
|
if (!existsSync18(input.worktreePath)) return null;
|
|
3262
3361
|
if (input.runId && input.workerName) {
|
|
3263
|
-
const
|
|
3362
|
+
const workerDir = path21.join(
|
|
3264
3363
|
input.harnessRoot,
|
|
3265
3364
|
"runs",
|
|
3266
3365
|
input.runId,
|
|
3267
3366
|
"workers",
|
|
3268
|
-
input.workerName
|
|
3269
|
-
"heartbeat.jsonl"
|
|
3367
|
+
input.workerName
|
|
3270
3368
|
);
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3369
|
+
const worker = readJson(
|
|
3370
|
+
path21.join(workerDir, "worker.json"),
|
|
3371
|
+
void 0
|
|
3372
|
+
);
|
|
3373
|
+
if (worker && heartbeatContentIsFresh(worker, now, heartbeatFreshMs)) {
|
|
3374
|
+
return "active_worker";
|
|
3275
3375
|
}
|
|
3276
3376
|
}
|
|
3277
|
-
const gitDir =
|
|
3377
|
+
const gitDir = path21.join(input.worktreePath, ".git");
|
|
3278
3378
|
if (!existsSync18(gitDir)) return null;
|
|
3279
3379
|
const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
|
|
3280
3380
|
if (porcelain.status !== 0) return "pr_or_unmerged_commits";
|
|
@@ -3305,8 +3405,8 @@ function assessOrphanWorktreeSafety(input) {
|
|
|
3305
3405
|
|
|
3306
3406
|
// src/harness-storage-snapshot.ts
|
|
3307
3407
|
init_paths();
|
|
3308
|
-
import { existsSync as existsSync19, readdirSync as
|
|
3309
|
-
import
|
|
3408
|
+
import { existsSync as existsSync19, readdirSync as readdirSync10, statSync as statSync9 } from "node:fs";
|
|
3409
|
+
import path22 from "node:path";
|
|
3310
3410
|
function harnessStorageSnapshot(opts = {}) {
|
|
3311
3411
|
const harnessRoot = normalizeHarnessRoot(opts.harnessRoot ?? resolveHarnessRoot());
|
|
3312
3412
|
const worktreesDir = harnessWorktreesDir(harnessRoot);
|
|
@@ -3329,7 +3429,7 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
3329
3429
|
let oldestMs = null;
|
|
3330
3430
|
let entries;
|
|
3331
3431
|
try {
|
|
3332
|
-
entries =
|
|
3432
|
+
entries = readdirSync10(worktreesDir, { withFileTypes: true });
|
|
3333
3433
|
} catch {
|
|
3334
3434
|
return {
|
|
3335
3435
|
harnessRoot,
|
|
@@ -3344,14 +3444,14 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
3344
3444
|
for (const runEntry of entries) {
|
|
3345
3445
|
if (!runEntry.isDirectory()) continue;
|
|
3346
3446
|
runCount += 1;
|
|
3347
|
-
const runPath =
|
|
3447
|
+
const runPath = path22.join(worktreesDir, runEntry.name);
|
|
3348
3448
|
try {
|
|
3349
|
-
const st =
|
|
3449
|
+
const st = statSync9(runPath);
|
|
3350
3450
|
oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
|
|
3351
3451
|
} catch {
|
|
3352
3452
|
}
|
|
3353
3453
|
try {
|
|
3354
|
-
for (const workerEntry of
|
|
3454
|
+
for (const workerEntry of readdirSync10(runPath, { withFileTypes: true })) {
|
|
3355
3455
|
if (workerEntry.isDirectory()) workerCount += 1;
|
|
3356
3456
|
}
|
|
3357
3457
|
} catch {
|
|
@@ -3382,10 +3482,10 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
3382
3482
|
init_paths();
|
|
3383
3483
|
import { existsSync as existsSync20 } from "node:fs";
|
|
3384
3484
|
import { homedir as homedir4 } from "node:os";
|
|
3385
|
-
import
|
|
3485
|
+
import path23 from "node:path";
|
|
3386
3486
|
var WELL_KNOWN_HARNESS_SCAN_ROOTS = [
|
|
3387
3487
|
"/var/tmp/kynver-harness",
|
|
3388
|
-
|
|
3488
|
+
path23.join(homedir4(), ".openclaw", "harness")
|
|
3389
3489
|
];
|
|
3390
3490
|
function addRoot(seen, roots, candidate) {
|
|
3391
3491
|
if (!candidate?.trim()) return;
|
|
@@ -3407,7 +3507,7 @@ function resolveHarnessScanRoots(options = {}) {
|
|
|
3407
3507
|
for (const candidate of extra ?? []) addRoot(seen, roots, candidate);
|
|
3408
3508
|
if (shouldScanWellKnownRoots(options)) {
|
|
3409
3509
|
for (const candidate of WELL_KNOWN_HARNESS_SCAN_ROOTS) {
|
|
3410
|
-
const resolved =
|
|
3510
|
+
const resolved = path23.resolve(candidate);
|
|
3411
3511
|
if (!seen.has(resolved) && existsSync20(resolved)) addRoot(seen, roots, resolved);
|
|
3412
3512
|
}
|
|
3413
3513
|
}
|
|
@@ -3486,15 +3586,52 @@ var CleanupGitRevCache = class {
|
|
|
3486
3586
|
}
|
|
3487
3587
|
};
|
|
3488
3588
|
|
|
3589
|
+
// src/cleanup-git-probe.ts
|
|
3590
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
3591
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
3592
|
+
import path24 from "node:path";
|
|
3593
|
+
var CLEANUP_GIT_PROBE_TIMEOUT_MS = 5e3;
|
|
3594
|
+
function cleanupGitCapture(cwd, args) {
|
|
3595
|
+
if (!existsSync21(path24.join(cwd, ".git"))) {
|
|
3596
|
+
return { status: null, stdout: "", stderr: "", error: "not_a_git_repo" };
|
|
3597
|
+
}
|
|
3598
|
+
try {
|
|
3599
|
+
const res = spawnSync3("git", args, {
|
|
3600
|
+
cwd,
|
|
3601
|
+
encoding: "utf8",
|
|
3602
|
+
timeout: CLEANUP_GIT_PROBE_TIMEOUT_MS
|
|
3603
|
+
});
|
|
3604
|
+
const timedOut = res.error?.message?.includes("ETIMEDOUT") === true;
|
|
3605
|
+
return {
|
|
3606
|
+
status: timedOut ? null : res.status,
|
|
3607
|
+
stdout: res.stdout || "",
|
|
3608
|
+
stderr: res.stderr || "",
|
|
3609
|
+
error: timedOut ? "git_timeout" : res.error ? res.error.message : null
|
|
3610
|
+
};
|
|
3611
|
+
} catch (error) {
|
|
3612
|
+
return {
|
|
3613
|
+
status: null,
|
|
3614
|
+
stdout: "",
|
|
3615
|
+
stderr: "",
|
|
3616
|
+
error: error.message
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
function gitStatusShortForCleanup(worktreePath) {
|
|
3621
|
+
const captured = cleanupGitCapture(worktreePath, ["status", "--short"]);
|
|
3622
|
+
if (captured.error === "not_a_git_repo") return [];
|
|
3623
|
+
if (captured.error === "git_timeout" || captured.status !== 0) return null;
|
|
3624
|
+
return captured.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3489
3627
|
// src/cleanup-git-status-cache.ts
|
|
3490
|
-
init_git();
|
|
3491
3628
|
var CleanupGitStatusCache = class {
|
|
3492
3629
|
cache = /* @__PURE__ */ new Map();
|
|
3493
3630
|
porcelain(worktreePath) {
|
|
3494
3631
|
const resolved = worktreePath;
|
|
3495
3632
|
const cached = this.cache.get(resolved);
|
|
3496
3633
|
if (cached !== void 0) return cached;
|
|
3497
|
-
const lines =
|
|
3634
|
+
const lines = gitStatusShortForCleanup(resolved) ?? [];
|
|
3498
3635
|
this.cache.set(resolved, lines);
|
|
3499
3636
|
return lines;
|
|
3500
3637
|
}
|
|
@@ -3599,9 +3736,9 @@ function mergeWorktreeIndexes(scanRoots) {
|
|
|
3599
3736
|
}
|
|
3600
3737
|
function worktreePathForCandidate(candidate, worktreesDir) {
|
|
3601
3738
|
if (candidate.runId && candidate.worker) {
|
|
3602
|
-
return
|
|
3739
|
+
return path25.join(worktreesDir, candidate.runId, candidate.worker);
|
|
3603
3740
|
}
|
|
3604
|
-
return
|
|
3741
|
+
return path25.resolve(candidate.path, "..");
|
|
3605
3742
|
}
|
|
3606
3743
|
function runHarnessCleanup(options = {}) {
|
|
3607
3744
|
let retention = resolveHarnessRetention(options);
|
|
@@ -3630,7 +3767,8 @@ function runHarnessCleanup(options = {}) {
|
|
|
3630
3767
|
for (const harnessRoot of paths.scanRoots) {
|
|
3631
3768
|
if (atSweepCap()) break;
|
|
3632
3769
|
emitCleanupProgress("root", harnessRoot);
|
|
3633
|
-
const worktreesDir =
|
|
3770
|
+
const worktreesDir = path25.join(harnessRoot, "worktrees");
|
|
3771
|
+
const rootIndex = filterWorktreeIndexForRoot(index, harnessRoot);
|
|
3634
3772
|
const scanOpts = {
|
|
3635
3773
|
harnessRoot,
|
|
3636
3774
|
worktreesDir,
|
|
@@ -3638,11 +3776,14 @@ function runHarnessCleanup(options = {}) {
|
|
|
3638
3776
|
worktreesAgeMs: retention.worktreesAgeMs,
|
|
3639
3777
|
includeOrphans: retention.includeOrphans,
|
|
3640
3778
|
runIdFilter: retention.runIdFilter,
|
|
3641
|
-
index,
|
|
3779
|
+
index: rootIndex,
|
|
3642
3780
|
now: paths.now
|
|
3643
3781
|
};
|
|
3644
|
-
const dependencyCandidates = scanDependencyCacheCandidates(scanOpts);
|
|
3645
|
-
emitCleanupProgress(
|
|
3782
|
+
const dependencyCandidates = retention.scanDependencyCaches ? scanDependencyCacheCandidates(scanOpts) : [];
|
|
3783
|
+
emitCleanupProgress(
|
|
3784
|
+
"dependency",
|
|
3785
|
+
retention.scanDependencyCaches ? `${dependencyCandidates.length} cache candidate(s) at ${harnessRoot}` : "skipped (worktree-only sweep)"
|
|
3786
|
+
);
|
|
3646
3787
|
let dependencyProcessed = 0;
|
|
3647
3788
|
for (const raw of dependencyCandidates) {
|
|
3648
3789
|
if (atSweepCap()) break;
|
|
@@ -3650,7 +3791,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3650
3791
|
if (dependencyProcessed % 50 === 0) {
|
|
3651
3792
|
emitCleanupProgress("dependency", `${dependencyProcessed}/${dependencyCandidates.length} evaluated`);
|
|
3652
3793
|
}
|
|
3653
|
-
const resolved =
|
|
3794
|
+
const resolved = path25.resolve(raw.path);
|
|
3654
3795
|
if (processedPaths.has(resolved)) continue;
|
|
3655
3796
|
processedPaths.add(resolved);
|
|
3656
3797
|
const candidate = { ...raw, path: resolved };
|
|
@@ -3661,7 +3802,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3661
3802
|
continue;
|
|
3662
3803
|
}
|
|
3663
3804
|
const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
|
|
3664
|
-
const indexed =
|
|
3805
|
+
const indexed = rootIndex.get(path25.resolve(worktreePath)) ?? null;
|
|
3665
3806
|
const guardReason = skipDependencyCacheRemoval({
|
|
3666
3807
|
indexed,
|
|
3667
3808
|
includeOrphans: true,
|
|
@@ -3684,9 +3825,9 @@ function runHarnessCleanup(options = {}) {
|
|
|
3684
3825
|
)
|
|
3685
3826
|
);
|
|
3686
3827
|
}
|
|
3687
|
-
for (const raw of scanBuildCacheCandidates(scanOpts)) {
|
|
3828
|
+
if (retention.scanDependencyCaches) for (const raw of scanBuildCacheCandidates(scanOpts)) {
|
|
3688
3829
|
if (atSweepCap()) break;
|
|
3689
|
-
const resolved =
|
|
3830
|
+
const resolved = path25.resolve(raw.path);
|
|
3690
3831
|
if (processedPaths.has(resolved)) continue;
|
|
3691
3832
|
processedPaths.add(resolved);
|
|
3692
3833
|
const candidate = { ...raw, path: resolved };
|
|
@@ -3697,7 +3838,7 @@ function runHarnessCleanup(options = {}) {
|
|
|
3697
3838
|
continue;
|
|
3698
3839
|
}
|
|
3699
3840
|
const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
|
|
3700
|
-
const indexed =
|
|
3841
|
+
const indexed = rootIndex.get(path25.resolve(worktreePath)) ?? null;
|
|
3701
3842
|
const guardReason = skipBuildCacheRemoval({
|
|
3702
3843
|
indexed,
|
|
3703
3844
|
includeOrphans: true,
|
|
@@ -3733,11 +3874,11 @@ function runHarnessCleanup(options = {}) {
|
|
|
3733
3874
|
if (worktreeProcessed % 50 === 0) {
|
|
3734
3875
|
emitCleanupProgress("worktrees", `${worktreeProcessed}/${worktreeCandidates.length} evaluated`);
|
|
3735
3876
|
}
|
|
3736
|
-
const resolved =
|
|
3877
|
+
const resolved = path25.resolve(raw.path);
|
|
3737
3878
|
if (worktreeSeen.has(resolved)) continue;
|
|
3738
3879
|
worktreeSeen.add(resolved);
|
|
3739
3880
|
const candidate = { ...raw, path: resolved };
|
|
3740
|
-
const indexed =
|
|
3881
|
+
const indexed = rootIndex.get(path25.resolve(candidate.path)) ?? null;
|
|
3741
3882
|
const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
|
|
3742
3883
|
worktreePath: candidate.path,
|
|
3743
3884
|
harnessRoot,
|
|
@@ -3747,14 +3888,17 @@ function runHarnessCleanup(options = {}) {
|
|
|
3747
3888
|
});
|
|
3748
3889
|
const guardSkip = skipWorktreeRemoval({
|
|
3749
3890
|
indexed,
|
|
3750
|
-
worktreePath:
|
|
3891
|
+
worktreePath: path25.resolve(candidate.path),
|
|
3751
3892
|
includeOrphans: retention.includeOrphans,
|
|
3752
3893
|
worktreesAgeMs: retention.worktreesAgeMs,
|
|
3753
3894
|
terminalWorktreesAgeMs: retention.terminalWorktreesAgeMs,
|
|
3754
3895
|
ageMs: candidate.ageMs,
|
|
3755
3896
|
orphanSafety,
|
|
3756
3897
|
worktreeRemovalGuard: options.worktreeRemovalGuard,
|
|
3757
|
-
liveness
|
|
3898
|
+
liveness,
|
|
3899
|
+
now: paths.now,
|
|
3900
|
+
harnessRoot,
|
|
3901
|
+
writeSalvageEvidence: retention.execute
|
|
3758
3902
|
});
|
|
3759
3903
|
if (guardSkip) {
|
|
3760
3904
|
const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
|
|
@@ -3779,11 +3923,11 @@ function runHarnessCleanup(options = {}) {
|
|
|
3779
3923
|
now: paths.now
|
|
3780
3924
|
})) {
|
|
3781
3925
|
if (atSweepCap()) break;
|
|
3782
|
-
const resolved =
|
|
3926
|
+
const resolved = path25.resolve(raw.path);
|
|
3783
3927
|
if (processedPaths.has(resolved)) continue;
|
|
3784
3928
|
processedPaths.add(resolved);
|
|
3785
3929
|
const candidate = { ...raw, path: resolved };
|
|
3786
|
-
const runId = candidate.runId ??
|
|
3930
|
+
const runId = candidate.runId ?? path25.basename(resolved);
|
|
3787
3931
|
const dirSkip = skipRunDirectoryRemoval({
|
|
3788
3932
|
harnessRoot,
|
|
3789
3933
|
runId,
|