openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
335 lines (334 loc) • 14.6 kB
JavaScript
import { d as toErrorObject } from "./error-coercion-W9BtigRm.mjs";
import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { i as resolveRuntimeWorkerUrl } from "./runtime-worker-url-CFqA9Dvi.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-De68tE_W.mjs";
import { r as withGitProcessOperation } from "./spawn-diagnostics-D1OSGice.mjs";
import { r as WorkerTaskError } from "./src-CzdT_kJd.mjs";
import { t as WorkerTaskPool } from "./worker-task-pool-r__tVCXa.mjs";
import { p as startGitOperationTiming } from "./git-exec-BOEpfMeZ.mjs";
import { t as ownedWorkerBytes } from "./worker-transfer-bytes-D_DP0IHa.mjs";
import { o as restoreGitWorkerFailure, s as serializeGitWorkerFailure } from "./git-worker-context-DDPUBONa.mjs";
import { f as runGitBuffered, p as runGitBytes } from "./git-D3obqZyl.mjs";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import { createHash } from "node:crypto";
import { performance } from "node:perf_hooks";
//#region src/infra/git-content-budget.ts
const budget = resolveGlobalSingleton(Symbol.for("openclaw.gitContentBudget"), () => ({
active: 0,
waiters: /* @__PURE__ */ new Set()
}));
async function withContentGitSlot(run, signal) {
signal?.throwIfAborted();
await new Promise((resolve, reject) => {
const start = () => {
budget.waiters.delete(start);
signal?.removeEventListener("abort", abort);
budget.active++;
resolve();
};
const abort = () => {
budget.waiters.delete(start);
reject(toErrorObject(signal?.reason, "Git operation aborted"));
};
if (budget.active < 2) start();
else {
budget.waiters.add(start);
signal?.addEventListener("abort", abort, { once: true });
}
});
try {
signal?.throwIfAborted();
return await run();
} finally {
budget.active--;
budget.waiters.values().next().value?.();
}
}
//#endregion
//#region src/infra/git-worker.ts
const POOL_OWNERS = [
"reads",
"content",
"workspace",
"worktrees",
"worktreeMaintenance"
];
const MAX_PENDING_OPERATIONS = 128;
const WORKER_PHASE_TIMEOUT_MS = 18e5;
const log = createSubsystemLogger("git/worker");
const SPAWN_OPERATIONS = {
"repository.identities": "repository.identities",
"repository.branches": "repository.branches",
"checkout.revision": "checkout.revision",
"checkout.context": "checkout.context",
"checkout.diff": "checkout.diff",
"checkout.baseline": "checkout.baseline",
"pull-request.branch-facts": "pull-request.branch-facts",
"worktree.snapshot": "worktree.snapshot",
"worktree.snapshot-verify-exact": "worktree.snapshot",
"worktree.cleanup-inspection": "worktree.cleanup",
"worktree.cleanup-fingerprint": "worktree.cleanup",
"worktree.eviction-classify": "worktree.cleanup",
"worktree.eviction-source": "worktree.cleanup",
"worktree.eviction-repositories": "worktree.cleanup",
"worktree.eviction-purge": "worktree.cleanup",
"worktree.provisioning-inspection": "worktree.provision",
"worktree.git-size": "worktree.inspect",
"worktree.checkout-transition-size": "worktree.inspect",
"worktree.directory-size": "worktree.inspect",
"workspace.artifacts": "workspace.inventory",
"workspace.inventory.select": "workspace.inventory",
"workspace.inventory.existing": "workspace.inventory",
"workspace.inventory.paths": "workspace.inventory",
"workspace.inventory.staged-directories": "workspace.inventory",
"workspace.manifest.capture": "workspace.manifest",
"workspace.manifest.snapshot": "workspace.manifest",
"workspace.manifest.parse": "workspace.manifest",
"workspace.manifest.serialize": "workspace.manifest",
"workspace.manifest.overlay": "workspace.manifest",
"workspace.manifest.pair": "workspace.manifest",
"workspace.manifest.staged": "workspace.manifest",
"workspace.manifest.entries": "workspace.manifest",
"workspace.manifest.file": "workspace.manifest",
"workspace.manifest.nodes": "workspace.manifest",
"workspace.manifest.stage-input": "workspace.manifest",
"workspace.manifest.tree-input": "workspace.manifest",
"workspace.manifest.remote-capture": "workspace.manifest",
"workspace.reconcile.preflight": "workspace.manifest"
};
function trackPending(pending, operation) {
pending.add(operation);
const settled = () => pending.delete(operation);
operation.then(settled, settled);
}
function runtime() {
return resolveGlobalSingleton(Symbol.for("openclaw.gitOperations"), () => ({ pending: /* @__PURE__ */ new Set() }), (state) => {
state.closing ??= (async () => {
await Promise.all(POOL_OWNERS.map((owner) => state[owner]?.close() ?? Promise.resolve()));
await Promise.allSettled(state.pending);
for (const owner of POOL_OWNERS) state[owner] = void 0;
})().finally(() => {
state.closing = void 0;
});
return state.closing;
});
}
function poolFor(state, command, contentRead) {
const owner = command.type === "worktree.snapshot" || command.type === "worktree.cleanup-inspection" || command.type === "worktree.cleanup-fingerprint" || command.type === "worktree.eviction-classify" || command.type === "worktree.eviction-repositories" || command.type === "worktree.eviction-purge" ? "worktreeMaintenance" : command.type.startsWith("worktree.") ? "worktrees" : command.type.startsWith("workspace.") ? "workspace" : contentRead ? "content" : "reads";
return state[owner] ??= new WorkerTaskPool({
workerUrl: resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.gitOperations),
workerClass: owner === "reads" || owner === "content" ? "reader" : owner === "workspace" ? "compute" : "writer",
workerOptions: owner === "workspace" ? { resourceLimits: { maxOldGenerationSizeMb: 1024 } } : void 0,
sharedCompute: owner === "workspace",
idleTimeoutMs: 3e4
});
}
/** The host retains processes and authority; workers own bounded inventory and projection work. */
async function runGitWorkerOperation(command, options = {}) {
const state = runtime();
if (state.closing || state.pending.size >= MAX_PENDING_OPERATIONS) throw new WorkerTaskError("Git operation capacity is unavailable; retry the request.", "unavailable");
options.signal?.throwIfAborted();
options.assertCurrent?.();
const transferList = options.transferList?.(command);
const admitted = transferList ? structuredClone(command, { transfer: [...new Set(transferList)] }) : structuredClone(command);
const baseEnv = { ...process.env };
admitted.filesystemRefs = options.git === void 0 && !Object.entries(baseEnv).some(([key, value]) => value !== void 0 && (/^(GIT_DIR|GIT_WORK_TREE|GIT_COMMON_DIR|GIT_CEILING_DIRECTORIES|GIT_DISCOVERY_ACROSS_FILESYSTEM|GIT_NAMESPACE|GIT_SHALLOW_FILE|GIT_GRAFT_FILE|GIT_REPLACE_REF_BASE|GIT_NO_REPLACE_OBJECTS|GIT_INDEX_FILE|GIT_OBJECT_DIRECTORY|GIT_ALTERNATE_OBJECT_DIRECTORIES|GIT_CONFIG_PARAMETERS)$/i.test(key) || /^(HOME|XDG_CONFIG_HOME|GIT_CONFIG_GLOBAL|GIT_CONFIG_SYSTEM)$/i.test(key) && /[\r\n]/u.test(value))) ? createHash("sha256").update(JSON.stringify(Object.entries(baseEnv).filter(([key]) => /^(GIT_|HOME$|XDG_CONFIG_HOME$|PATH$|SUDO_UID$)/i.test(key)))).digest("hex") : void 0;
const operation = executeOperation(state, admitted, baseEnv, {
...options,
git: options.git ? {
text: options.git.text,
buffered: options.git.buffered
} : void 0
});
trackPending(state.pending, operation);
return operation;
}
async function executeOperation(state, command, baseEnv, options) {
const contentRoot = command.type === "checkout.diff" || command.type === "checkout.baseline" ? command.input.cwd : command.type === "pull-request.branch-facts" ? command.input.root : void 0;
const contentRead = contentRoot !== void 0;
const snapshot = command.type === "worktree.snapshot" || command.type === "worktree.snapshot-verify-exact";
const contentGit = contentRead || snapshot;
if (snapshot) {
baseEnv.GIT_NO_LAZY_FETCH = "1";
baseEnv.GIT_ALLOW_PROTOCOL = "";
}
let gitCommandCount = 0;
let summedGitWallMs = 0;
let summedGitQueueWaitMs = 0;
let gitStdoutBytes = 0;
let gitStderrBytes = 0;
let gitTimeoutCount = 0;
let workerQueueWaitMs = null;
let slowestGitCommand;
const timing = contentRoot !== void 0 ? startGitOperationTiming("content-read", log, () => ({
operation: command.type,
checkoutId: createHash("sha256").update(path.resolve(contentRoot)).digest("hex").slice(0, 16),
checkoutClass: command.type === "pull-request.branch-facts" && command.input.refreshIndex ? "managed" : "unspecified",
gitCommandCount,
workerQueueWaitMs,
summedGitWallMs: Math.round(summedGitWallMs),
summedGitQueueWaitMs: Math.round(summedGitQueueWaitMs),
gitStdoutBytes,
gitStderrBytes,
gitTimeoutCount,
slowestGitCommand
})) : void 0;
let firstHostRequest = true;
let outcome = "threw";
const hostWork = /* @__PURE__ */ new Set();
const temporaryDirectories = /* @__PURE__ */ new Set();
const hostErrors = /* @__PURE__ */ new Map();
let errorSequence = 0;
const request = async (value, signal) => {
try {
signal.throwIfAborted();
if (!isRecord(value) || typeof value.type !== "string" || !isRecord(value.input)) throw new Error("Invalid Git worker host request");
const effect = value;
let result;
const transferList = [];
if (effect.type === "git.text" || effect.type === "git.buffer") {
const run = effect.type === "git.text" ? options.git?.text ?? runGitBytes : options.git?.buffered ?? runGitBuffered;
const queuedAt = timing ? performance.now() : 0;
const execute = async () => {
const startedAt = timing ? performance.now() : 0;
let termination = "threw";
if (timing) {
gitCommandCount++;
summedGitQueueWaitMs += startedAt - queuedAt;
}
try {
const output = await withGitProcessOperation(SPAWN_OPERATIONS[command.type], () => run(effect.input.cwd, effect.input.args, {
...effect.input.options,
operation: SPAWN_OPERATIONS[command.type],
baseEnv,
signal,
beforeRun: options.assertCurrent,
killProcessTree: true,
lowerPriority: contentGit
}));
if (timing) {
termination = output.termination;
gitStdoutBytes += output.stdout.byteLength;
gitStderrBytes += output.stderr.byteLength;
gitTimeoutCount += Number(termination === "timeout");
}
return output;
} finally {
if (timing) {
const durationMs = Math.round(performance.now() - startedAt);
summedGitWallMs += durationMs;
if (!slowestGitCommand || durationMs > slowestGitCommand.durationMs) {
const args = effect.input.args;
let i = 0;
while (i < args.length && args[i].startsWith("-")) i += args[i] === "-c" || args[i] === "-C" ? 2 : 1;
const name = args[i] ?? "";
slowestGitCommand = {
command: /^(diff|ls-files|rev-parse|rev-list|merge-base|cat-file|symbolic-ref|for-each-ref|version|log|show|hash-object)$/.test(name) ? name : "other",
...name === "diff" ? { diffMode: args.includes("--no-index") ? "--no-index" : args.slice(i + 1).find((arg) => /^(--shortstat|--raw|--patch|--name-status)$/.test(arg)) ?? "other" } : {},
durationMs,
termination
};
}
}
}
};
const output = await (contentGit ? withContentGitSlot(execute, signal) : execute());
const stdout = ownedWorkerBytes(output.stdout);
const stderr = ownedWorkerBytes(output.stderr);
result = {
...output,
stdout,
stderr
};
transferList.push(stdout.buffer, stderr.buffer);
} else if (effect.type === "git.temporary-directory") {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-git-operation-"));
temporaryDirectories.add(directory);
result = directory;
} else if (effect.type === "workspace.inventory.write") {
if (!options.onInventoryChunk) throw new Error("Workspace inventory has no active output owner");
options.assertCurrent?.();
await options.onInventoryChunk(effect.input.bytes, { signal });
} else {
if (!options.onEffect) throw new Error("Git read operation requested a lifecycle effect");
options.assertCurrent?.();
result = await options.onEffect(effect, { signal });
}
return {
input: {
ok: true,
value: result
},
transferList: [...new Set(transferList)],
timeoutMs: WORKER_PHASE_TIMEOUT_MS
};
} catch (error) {
const origin = ++errorSequence;
hostErrors.set(origin, error);
return {
input: {
ok: false,
error: {
...serializeGitWorkerFailure(error),
origin
}
},
timeoutMs: WORKER_PHASE_TIMEOUT_MS
};
}
};
try {
const enqueuedAt = timing ? performance.now() : 0;
const input = timing ? () => {
workerQueueWaitMs = Math.round(performance.now() - enqueuedAt);
return command;
} : command;
const reply = await poolFor(state, command, contentRead).run(input, {
inputBytes: options.inputBytes,
transferList: options.transferList,
signal: options.signal,
timeoutMs: WORKER_PHASE_TIMEOUT_MS,
onRequest: (value, context) => {
if (firstHostRequest) {
firstHostRequest = false;
timing?.markPhase();
}
if (!isRecord(value) || value.type !== "git.batch" || !isRecord(value.input) || !Array.isArray(value.input.requests) || value.input.requests.length === 0 || value.input.requests.length > 16) throw new Error("Invalid Git worker request batch");
const pending = Promise.all(value.input.requests.map((item) => request(item, context.signal))).then((replies) => ({
input: replies.map((response) => response.input),
transferList: [...new Set(replies.flatMap((response) => response.transferList ?? []))],
timeoutMs: WORKER_PHASE_TIMEOUT_MS
}));
trackPending(hostWork, pending);
return pending;
}
});
timing?.markPhase();
if (!reply.ok) {
if (reply.error.origin !== void 0 && hostErrors.has(reply.error.origin)) throw hostErrors.get(reply.error.origin);
throw restoreGitWorkerFailure(reply.error);
}
options.signal?.throwIfAborted();
options.assertCurrent?.();
outcome = "returned";
return reply.value;
} finally {
let cleanupSucceeded = false;
try {
await Promise.allSettled(hostWork);
await Promise.all([...temporaryDirectories].map((directory) => fs.rm(directory, {
recursive: true,
force: true
})));
cleanupSucceeded = true;
} finally {
timing?.finish(cleanupSucceeded ? outcome : "threw");
}
}
}
//#endregion
export { withContentGitSlot as n, runGitWorkerOperation as t };