openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
259 lines (258 loc) • 12.7 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { t as VERSION } from "./version-v1kuAkGj.js";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js";
import { r as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BRgmrGHt.js";
import { l as withOpenClawStateStartupMigrationCheckpointDatabase, p as assertOpenClawStateWriteAllowed } from "./openclaw-state-db-BRTnL-D8.js";
import { i as isPidDefinitelyDead, t as getFileLockProcessStartTime } from "./pid-alive-XuW58Ofz.js";
import { createRequire } from "node:module";
import { hostname } from "node:os";
import { randomUUID } from "node:crypto";
import { performance } from "node:perf_hooks";
//#region src/infra/startup-migration-checkpoint.ts
const STARTUP_MIGRATION_META_KEY = "startup-migrations";
const STATE_MIGRATION_META_KEY = "state-migrations";
const STARTUP_MIGRATION_BUILD_SEPARATOR = "\n";
const STARTUP_MIGRATION_CHECKPOINT_FORMAT = "3";
const STARTUP_MIGRATION_LEASE_SCOPE = "startup-migrations";
const STARTUP_MIGRATION_LEASE_KEY = "global";
const STARTUP_MIGRATION_LEASE_POLL_INTERVAL_MS = 250;
const STARTUP_MIGRATION_LEASE_TTL_MS = 3e5;
var StartupMigrationLeaseConflictError = class extends Error {
constructor(message, canWaitForSameHostOwner) {
super(message);
this.canWaitForSameHostOwner = canWaitForSameHostOwner;
}
};
function parseStartupMigrationLeaseOwner(payloadJson) {
if (!payloadJson) return null;
let owner;
try {
const parsed = JSON.parse(payloadJson);
owner = isRecord(parsed) ? parsed.owner : null;
} catch {
return null;
}
if (!isRecord(owner)) return null;
const { pid, host, startedAt } = owner;
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0 || typeof host !== "string" || !host || startedAt !== null && (typeof startedAt !== "number" || !Number.isSafeInteger(startedAt) || startedAt < 0)) return null;
return {
pid,
host,
startedAt
};
}
function isStartupMigrationLeaseOwnerDefinitelyGone(owner) {
if (!owner || owner.host !== hostname()) return false;
if (isPidDefinitelyDead(owner.pid)) return true;
const currentStartedAt = getFileLockProcessStartTime(owner.pid);
return owner.startedAt !== null && currentStartedAt !== null && currentStartedAt !== owner.startedAt;
}
function resolveStartupMigrationBuildIdentity(moduleUrl = import.meta.url) {
try {
const require = createRequire(moduleUrl);
for (const candidate of [
"./build-info.json",
"../build-info.json",
"../../dist/build-info.json"
]) try {
const info = require(candidate);
if (typeof info.builtAt !== "string" || !info.builtAt.trim()) continue;
return info.builtAt.trim();
} catch {}
} catch {}
return null;
}
function withStartupMigrationCheckpointDatabase(env, callback) {
return withOpenClawStateStartupMigrationCheckpointDatabase(callback, { env });
}
function writeStartupMigrationCheckpointDatabase(env, callback) {
const databasePath = resolveOpenClawStateSqlitePath(env);
return withStartupMigrationCheckpointDatabase(env, (db) => runSqliteImmediateTransactionSync(db, () => {
assertOpenClawStateWriteAllowed({
database: db,
databasePath,
env
});
return callback(db);
}));
}
function assertStartupMigrationLeaseOwnedInTransaction(params) {
const stateDb = getNodeSqliteKysely(params.database);
if (!executeSqliteQueryTakeFirstSync(params.database, stateDb.selectFrom("state_leases").select("owner").where("scope", "=", STARTUP_MIGRATION_LEASE_SCOPE).where("lease_key", "=", STARTUP_MIGRATION_LEASE_KEY).where("owner", "=", params.owner).where("expires_at", ">", params.nowMs ?? Date.now()))) throw new Error("OpenClaw startup migration lease was lost before startup migrations completed; retry so migrations can run under a fresh lease.");
}
function formatStartupMigrationCheckpoint(params) {
const identity = params.identity;
if (params.buildIdentity === null || !identity || !identity.effectiveConfigFingerprint.trim() || !identity.pluginDoctorConfigFingerprint.trim() || !identity.pluginMigrationFingerprint.trim()) return null;
return [
params.version,
STARTUP_MIGRATION_CHECKPOINT_FORMAT,
params.buildIdentity,
identity.effectiveConfigFingerprint,
identity.pluginDoctorConfigFingerprint,
identity.pluginMigrationFingerprint
].join(STARTUP_MIGRATION_BUILD_SEPARATOR);
}
function readMigrationCheckpoints(env, metaKeys) {
return withStartupMigrationCheckpointDatabase(env, (db) => {
const stateDb = getNodeSqliteKysely(db);
return executeSqliteQuerySync(db, stateDb.selectFrom("schema_meta").select(["meta_key as metaKey", "app_version as appVersion"]).where("meta_key", "in", metaKeys)).rows;
});
}
function readStartupMigrationVersion(env = process.env) {
return readMigrationCheckpoints(env, [STARTUP_MIGRATION_META_KEY])[0]?.appVersion?.split(STARTUP_MIGRATION_BUILD_SEPARATOR, 1)[0] ?? null;
}
/** Returns whether the canonical automatic-migration lease is still live. */
function hasActiveStartupMigrationLease(params = {}) {
const env = params.env ?? process.env;
const nowMs = params.nowMs ?? Date.now();
return withExistingOpenClawStateDatabaseReadOnly(({ db }) => {
const stateDb = getNodeSqliteKysely(db);
const lease = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("state_leases").select("payload_json as payloadJson").where("scope", "=", STARTUP_MIGRATION_LEASE_SCOPE).where("lease_key", "=", STARTUP_MIGRATION_LEASE_KEY).where("expires_at", ">", nowMs));
return Boolean(lease && !isStartupMigrationLeaseOwnerDefinitelyGone(parseStartupMigrationLeaseOwner(lease.payloadJson)));
}, { env }) ?? false;
}
function readMigrationCheckpointStatus(params = {}) {
const env = params.env ?? process.env;
const checkpoint = formatStartupMigrationCheckpoint({
buildIdentity: params.buildIdentity === void 0 ? resolveStartupMigrationBuildIdentity() : params.buildIdentity,
identity: params.identity,
version: params.version ?? VERSION
});
if (checkpoint === null) return "stale";
const current = readMigrationCheckpoints(env, [STATE_MIGRATION_META_KEY, STARTUP_MIGRATION_META_KEY]).filter((row) => row.appVersion === checkpoint);
if (current.some((row) => row.metaKey === STARTUP_MIGRATION_META_KEY)) return "startup-current";
return current.length > 0 ? "state-current" : "stale";
}
function acquireStartupMigrationLease(params = {}) {
const env = params.env ?? process.env;
const nowMs = params.nowMs ?? Date.now();
const owner = params.owner ?? randomUUID();
const ownerPid = params.ownerPid ?? process.pid;
const leaseOwner = {
pid: ownerPid,
host: hostname(),
startedAt: getFileLockProcessStartTime(ownerPid)
};
const expiresAt = nowMs + STARTUP_MIGRATION_LEASE_TTL_MS;
writeStartupMigrationCheckpointDatabase(env, (db) => {
const stateDb = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, stateDb.deleteFrom("state_leases").where("scope", "=", STARTUP_MIGRATION_LEASE_SCOPE).where("lease_key", "=", STARTUP_MIGRATION_LEASE_KEY).where("expires_at", "<=", nowMs));
const existing = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("state_leases").select([
"owner",
"expires_at as expiresAt",
"payload_json as payloadJson"
]).where("scope", "=", STARTUP_MIGRATION_LEASE_SCOPE).where("lease_key", "=", STARTUP_MIGRATION_LEASE_KEY));
const existingOwner = parseStartupMigrationLeaseOwner(existing?.payloadJson ?? null);
if (existing && isStartupMigrationLeaseOwnerDefinitelyGone(existingOwner)) executeSqliteQuerySync(db, stateDb.deleteFrom("state_leases").where("scope", "=", STARTUP_MIGRATION_LEASE_SCOPE).where("lease_key", "=", STARTUP_MIGRATION_LEASE_KEY).where("owner", "=", existing.owner));
else if (existing) {
const ownerHint = existingOwner ? ` (held by pid ${existingOwner.pid})` : "";
throw new StartupMigrationLeaseConflictError(`OpenClaw startup migrations are already running for this state directory; retry after the other OpenClaw process finishes or after ${new Date(existing.expiresAt ?? expiresAt).toISOString()}.${ownerHint}`, existingOwner?.host === hostname());
}
executeSqliteQuerySync(db, stateDb.insertInto("state_leases").values({
scope: STARTUP_MIGRATION_LEASE_SCOPE,
lease_key: STARTUP_MIGRATION_LEASE_KEY,
owner,
expires_at: expiresAt,
heartbeat_at: nowMs,
payload_json: JSON.stringify({
version: VERSION,
owner: leaseOwner
}),
created_at: nowMs,
updated_at: nowMs
}));
});
return {
owner,
assertOwnedInTransaction: (database, assertionParams = {}) => {
assertStartupMigrationLeaseOwnedInTransaction({
database,
owner,
nowMs: assertionParams.nowMs
});
},
heartbeat: (heartbeatParams = {}) => {
const heartbeatNowMs = heartbeatParams.nowMs ?? Date.now();
const heartbeatExpiresAt = heartbeatNowMs + STARTUP_MIGRATION_LEASE_TTL_MS;
writeStartupMigrationCheckpointDatabase(env, (db) => {
const stateDb = getNodeSqliteKysely(db);
if (executeSqliteQuerySync(db, stateDb.updateTable("state_leases").set({
expires_at: heartbeatExpiresAt,
heartbeat_at: heartbeatNowMs,
updated_at: heartbeatNowMs
}).where("scope", "=", STARTUP_MIGRATION_LEASE_SCOPE).where("lease_key", "=", STARTUP_MIGRATION_LEASE_KEY).where("owner", "=", owner).where("expires_at", ">", heartbeatNowMs)).numAffectedRows !== 1n) throw new Error("OpenClaw startup migration lease was lost before startup migrations completed; retry so migrations can run under a fresh lease.");
});
},
release: () => {
writeStartupMigrationCheckpointDatabase(env, (db) => {
const stateDb = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, stateDb.deleteFrom("state_leases").where("scope", "=", STARTUP_MIGRATION_LEASE_SCOPE).where("lease_key", "=", STARTUP_MIGRATION_LEASE_KEY).where("owner", "=", owner));
});
}
};
}
async function acquireStartupMigrationLeaseWithWait(params = {}) {
const now = params.now ?? Date.now;
const monotonicNow = params.monotonicNow ?? performance.now.bind(performance);
const sleep = params.sleep ?? (async (ms) => await new Promise((resolve) => {
setTimeout(resolve, ms);
}));
const timeoutMs = Math.max(0, Math.min(params.timeoutMs ?? 3e5, STARTUP_MIGRATION_LEASE_TTL_MS));
const pollIntervalMs = Math.max(1, params.pollIntervalMs ?? STARTUP_MIGRATION_LEASE_POLL_INTERVAL_MS);
const owner = params.owner ?? randomUUID();
const deadlineMs = monotonicNow() + timeoutMs;
while (true) try {
return acquireStartupMigrationLease({
env: params.env,
nowMs: now(),
owner,
ownerPid: params.ownerPid
});
} catch (error) {
if (!(error instanceof StartupMigrationLeaseConflictError) || !error.canWaitForSameHostOwner) throw error;
const remainingMs = deadlineMs - monotonicNow();
if (remainingMs <= 0) throw error;
await sleep(Math.min(pollIntervalMs, remainingMs));
}
}
function recordSuccessfulMigrationCheckpoints(metaKeys, params = {}) {
const env = params.env ?? process.env;
const version = params.version ?? VERSION;
const buildIdentity = params.buildIdentity === void 0 ? resolveStartupMigrationBuildIdentity() : params.buildIdentity;
const nowMs = params.nowMs ?? Date.now();
const checkpoint = formatStartupMigrationCheckpoint({
buildIdentity,
identity: params.identity,
version
});
if (checkpoint === null) return;
writeStartupMigrationCheckpointDatabase(env, (db) => {
params.lease?.assertOwnedInTransaction(db, { nowMs });
const stateDb = getNodeSqliteKysely(db);
for (const metaKey of metaKeys) executeSqliteQuerySync(db, stateDb.insertInto("schema_meta").values({
meta_key: metaKey,
role: "global",
schema_version: 3,
agent_id: null,
app_version: checkpoint,
created_at: nowMs,
updated_at: nowMs
}).onConflict((conflict) => conflict.column("meta_key").doUpdateSet({
role: "global",
schema_version: 3,
agent_id: null,
app_version: checkpoint,
updated_at: nowMs
})));
});
}
function recordSuccessfulStateMigrations(params = {}) {
recordSuccessfulMigrationCheckpoints([STATE_MIGRATION_META_KEY], params);
}
function recordSuccessfulStartupMigrations(params = {}) {
recordSuccessfulMigrationCheckpoints([STATE_MIGRATION_META_KEY, STARTUP_MIGRATION_META_KEY], params);
}
//#endregion
export { readMigrationCheckpointStatus as a, recordSuccessfulStateMigrations as c, hasActiveStartupMigrationLease as i, acquireStartupMigrationLease as n, readStartupMigrationVersion as o, acquireStartupMigrationLeaseWithWait as r, recordSuccessfulStartupMigrations as s, STARTUP_MIGRATION_LEASE_TTL_MS as t };