openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
266 lines (265 loc) • 9.91 kB
JavaScript
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs";
import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-DfifnSGp.mjs";
import { g as withSqliteReadOnlyWorkerScope } from "./sqlite-readonly-worker-TSy0m8v6.mjs";
import { i as sameFileMutationFingerprint } from "./file-descriptor-B4yBUiAP.mjs";
import { n as readSqliteIntegrityFileIdentity } from "./sqlite-file-generation-C_XgOkXx.mjs";
import "./openclaw-agent-db-contract-ClUH-Vmp.mjs";
import { r as cloneEnvWithPlatformSemantics } from "./config-env-vars-DjPHKieF.mjs";
import { c as createAgentDatabaseInspectionRefusal, h as preparePendingAgentDatabase, m as listAgentDatabaseAdmissionRefusals, u as failPendingAgentDatabase } from "./agent-database-admission-dWNS6Rcs.mjs";
import { o as readAgentDeletionJournalStatusInWorker } from "./agent-deletion-journal.read-Bkjt0nDs.mjs";
import { statSync } from "node:fs";
import { AsyncLocalStorage } from "node:async_hooks";
//#region src/state/agent-database-startup.ts
function readSchemaSourceWitness(pathname) {
try {
const files = [
"",
"-wal",
"-journal"
].map((suffix) => statSync(`${pathname}${suffix}`, {
bigint: true,
throwIfNoEntry: false
}));
return files[0] && files.every((file) => !file || file.isFile()) ? files : void 0;
} catch {
return;
}
}
function matchesSchemaSourceWitness(before, after) {
return Boolean(after && before.every((file, index) => {
const current = after[index];
return file ? current && sameFileMutationFingerprint(file, current) : !current;
}));
}
const log = createSubsystemLogger("state/agent-admission");
const startupAdmission = new AsyncLocalStorage();
/** Startup owns readers until the Gateway adopts them; only the Gateway activates agents. */
var AgentDatabaseStartupAdmission = class {
constructor(deferInspections = true) {
this.deferInspections = deferInspections;
this.controller = new AbortController();
this.activation = createDeferredCore();
this.work = /* @__PURE__ */ new Set();
this.pending = /* @__PURE__ */ new Map();
this.adopted = false;
this.activated = false;
this.stopped = false;
this.preparation = Promise.resolve();
}
get signal() {
return this.controller.signal;
}
get isStopped() {
return this.stopped;
}
/** Full readiness stays fresh; only unchanged compatibility headers cross into bootstrap. */
prepareSchemaHeaders(env) {
const prepared = {
statePath: resolveOpenClawStateSqlitePath(env),
headers: /* @__PURE__ */ new Map()
};
this.preparedSchemaHeaders = prepared;
return (pathname) => {
const before = readSchemaSourceWitness(pathname);
return (version) => {
if (!this.stopped && this.preparedSchemaHeaders === prepared && before && version === 24 && matchesSchemaSourceWitness(before, readSchemaSourceWitness(pathname))) prepared.headers.set(pathname, {
version,
witness: before
});
};
};
}
takePreparedSchemaHeaders(env) {
const prepared = this.preparedSchemaHeaders;
this.preparedSchemaHeaders = void 0;
return (pathname, supportedVersion) => {
const header = prepared?.headers.get(pathname);
return !this.stopped && prepared?.statePath === resolveOpenClawStateSqlitePath(env) && header?.version === supportedVersion && matchesSchemaSourceWitness(header.witness, readSchemaSourceWitness(pathname)) ? { version: header.version } : void 0;
};
}
track(work) {
this.work.add(work);
work.then(() => this.work.delete(work), () => this.work.delete(work));
}
scheduling(env) {
return {
signal: this.signal,
canDefer: (target) => this.deferInspections && target.agentId !== void 0,
track: (work) => this.track(work),
defer: (inspections, reason) => this.defer({
env,
inspections,
reason
})
};
}
recordInspectionFailure(target, inspection, error) {
this.signal.throwIfAborted();
if (!target.agentId) return false;
(inspection.agentRefusals ??= []).push(createAgentDatabaseInspectionRefusal({
agentId: target.agentId,
paths: [target.path],
reason: formatErrorMessage(error),
cause: error
}));
return true;
}
captureRefusals(env) {
return new Map(listAgentDatabaseAdmissionRefusals({ env }).filter((refusal) => this.pending.get(refusal.agentId) === refusal).map((refusal) => [refusal.agentId, refusal]));
}
reuseRefusal(target, inspection, priorRefusals) {
const refusal = target.agentId && priorRefusals?.get(target.agentId);
if (!refusal) return false;
(inspection.agentRefusals ??= []).push(refusal);
return true;
}
defer(params) {
this.signal.throwIfAborted();
const env = cloneEnvWithPlatformSemantics(params.env);
const grouped = /* @__PURE__ */ new Map();
for (const inspection of params.inspections) {
const agentId = inspection.target.agentId;
if (!agentId) throw new Error(`Cannot defer a database without an agent owner: ${inspection.target.path}`);
grouped.set(agentId, [...grouped.get(agentId) ?? [], inspection]);
}
const refusals = [];
for (const [agentId, inspections] of grouped) {
const paths = [...new Set(inspections.map(({ target }) => target.path))];
const refusal = createAgentDatabaseInspectionRefusal({
agentId,
paths,
pending: true,
reason: `Agent ${agentId} has not completed startup inspection and preparation. ${params.reason}`
});
this.pending.set(agentId, refusal);
refusals.push(refusal);
log.warn(refusal.reason, {
agentId,
paths,
repairHint: refusal.repairHint
});
const witnesses = paths.map((pathname) => {
try {
return {
pathname,
identity: readSqliteIntegrityFileIdentity(pathname)
};
} catch (error) {
return {
pathname,
error
};
}
});
const checked = Promise.allSettled(inspections.map(({ result }) => result));
const recovery = (async () => {
const results = await checked;
const activation = await this.activation.promise;
if (!activation || this.stopped) return;
const prepare = async () => {
const assertCurrent = () => {
this.signal.throwIfAborted();
if (!activation.isCurrent() || this.pending.get(agentId) !== refusal) throw new Error(`Gateway no longer owns preparation for agent ${agentId}`);
for (const witness of witnesses) {
if (!witness.identity) throw witness.error;
readSqliteIntegrityFileIdentity(witness.pathname, witness.identity);
}
};
const assertNotDeleted = async () => {
assertCurrent();
const deletion = await readAgentDeletionJournalStatusInWorker(agentId, { env }, this.signal);
assertCurrent();
if (deletion !== "absent") throw new Error(`Agent ${agentId} was deleted during startup inspection`);
};
try {
assertCurrent();
for (const result of results) {
if (result.status === "rejected") throw result.reason;
const inspection = result.value;
if (inspection.incompatible.length || inspection.indeterminate.length || inspection.agentRefusals?.length || inspection.pendingMigrations?.length) throw new Error(inspection.agentRefusals?.map((entry) => entry.reason).join("; ") || inspection.indeterminate.map((entry) => entry.reason).join("; ") || `Agent ${agentId} database requires Doctor before preparation`);
}
await withSqliteReadOnlyWorkerScope(async () => {
await assertNotDeleted();
await preparePendingAgentDatabase(refusal, {
env,
assertCurrent
}, async () => {
await activation.prepareAgent({
agentId,
paths,
env,
signal: this.signal,
assertCurrent
});
await assertNotDeleted();
});
}, {
signal: this.signal,
deadlineOwnedByCaller: true
});
log.info("agent database recovered after background inspection and preparation", {
agentId,
paths
});
} catch (error) {
if (!this.stopped) {
const reason = formatErrorMessage(error);
failPendingAgentDatabase(refusal, error, { env });
log.warn("agent database remains degraded", {
agentId,
paths,
reason
});
}
} finally {
if (this.pending.get(agentId) === refusal) this.pending.delete(agentId);
}
};
const prepared = this.preparation.then(prepare);
this.preparation = prepared.catch(() => {});
await prepared;
})();
this.track(recovery);
}
return refusals;
}
adopt() {
this.signal.throwIfAborted();
if (this.adopted) throw new Error("Agent database startup admission already belongs to a Gateway");
this.adopted = true;
return { stop: () => this.stop() };
}
activate(activation) {
if (!this.adopted || this.stopped || this.activated) return;
this.activated = true;
this.activation.resolve(activation);
}
async releaseStartup() {
if (!this.adopted) await this.stop();
}
stop() {
return this.stopping ??= (async () => {
this.stopped = true;
this.preparedSchemaHeaders = void 0;
this.controller.abort(/* @__PURE__ */ new Error("Gateway stopped during agent database inspection"));
this.activation.resolve(void 0);
while (this.work.size > 0) await Promise.allSettled(this.work);
})();
}
};
function getAgentDatabaseStartupAdmission() {
const scope = startupAdmission.getStore();
return scope?.isStopped ? void 0 : scope;
}
async function withAgentDatabaseStartupAdmission(run, options = {}) {
const admission = getAgentDatabaseStartupAdmission() ?? new AgentDatabaseStartupAdmission(options.deferInspections);
try {
return await startupAdmission.run(admission, () => run(admission));
} finally {
await admission.releaseStartup();
}
}
//#endregion
export { withAgentDatabaseStartupAdmission as n, getAgentDatabaseStartupAdmission as t };