openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
179 lines (178 loc) • 7.24 kB
JavaScript
import { o as racePromiseWithAbortSignal } from "./src-D1BHGNTj.mjs";
import "./abort-signal-QqAmLWTl.mjs";
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import "./openclaw-agent-db-contract-OYt2DCoK.mjs";
import { r as captureOpenClawStateWorkerContext } from "./openclaw-state-worker-context-BvvgY6-t.mjs";
import { n as prepareAgentDatabaseDeletionSnapshotRead } from "./agent-deletion-journal.read-BRl4UyPn.mjs";
import { T as runWithGatewayIndependentRootWorkAdmission, f as isGatewayRestartDrainError } from "./gateway-work-admission-DAV5kkgV.mjs";
import { o as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dd3VSz82.mjs";
import { c as resolveConfiguredAgentDatabaseTargets, o as resolveAllAgentSessionStoreTargetsSync } from "./targets-kx08xomQ.mjs";
import { r as getRuntimeConfig } from "./io.runtime-B134MOMz.mjs";
import "./config-VnHbY_QV.mjs";
import { t as getAgentDatabaseStartupAdmission } from "./agent-database-startup-BYk3phjn.mjs";
import { r as createRetainedAgentDatabaseMatcherFromSnapshot } from "./agent-deletion-discovery-DxqezB7f.mjs";
import { t as resolveCronJobsStorePath } from "./paths-Dqy-ZJ8r.mjs";
import { a as runSessionRegistryMaintenanceForStore } from "./sessions-CRUJeu5H.mjs";
import { r as loadCronJobsStore } from "./store-l4f-3zyW.mjs";
import { t as maintainCronRunHistory } from "./run-history-CXnjOwaN.mjs";
//#region src/cron/session-registry-maintenance.ts
const log$1 = createSubsystemLogger("cron/maintenance");
const SESSION_REGISTRY_RETENTION_MS = 6048e5;
function resolveExplicitCronSessionSegment(sessionKey) {
return /^(?:agent:[^:]+:)?cron:([^:]+)$/u.exec(sessionKey?.trim() ?? "")?.[1]?.toLowerCase();
}
async function readRunningCronJobIds() {
try {
const cronStorePath = resolveCronJobsStorePath();
const runningJobs = (await loadCronJobsStore(cronStorePath)).jobs.filter((job) => typeof job.state?.runningAtMs === "number");
const ids = /* @__PURE__ */ new Set();
for (const job of runningJobs) {
ids.add(job.id.toLowerCase());
if (job.sessionTarget === "main") continue;
const explicitSessionSegment = resolveExplicitCronSessionSegment(job.sessionKey);
if (explicitSessionSegment) ids.add(explicitSessionSegment);
}
return {
ok: true,
ids,
count: runningJobs.length
};
} catch (err) {
return {
ok: false,
reason: formatErrorMessage(err)
};
}
}
async function runSessionRegistryMaintenance(params) {
params.assertCurrent?.();
const cfg = getRuntimeConfig();
const runningCronJobs = await readRunningCronJobIds();
params.assertCurrent?.();
if (!runningCronJobs.ok) return {
retentionMs: SESSION_REGISTRY_RETENTION_MS,
runningCronJobs: 0,
pruned: 0,
skippedStores: 0,
stores: [],
skippedReason: `cron store unreadable: ${runningCronJobs.reason}`
};
const env = process.env;
return await prepareAgentDatabaseDeletionSnapshotRead({ env }).withCurrentSnapshot(async (snapshot, assertSnapshotCurrent) => {
const stores = [];
const assertCurrent = () => {
params.assertCurrent?.();
assertSnapshotCurrent();
};
assertCurrent();
const registeredDatabases = (snapshot?.registeredAgentDatabases ?? []).filter((entry) => entry.schemaVersion === 25);
const isRetained = createRetainedAgentDatabaseMatcherFromSnapshot(env, () => resolveConfiguredAgentDatabaseTargets(cfg, {
env,
registeredDatabases
}), snapshot);
const deletedAgents = new Map(snapshot?.deletedAgents.map(({ agentId, status }) => [agentId, status]));
for (const target of resolveAllAgentSessionStoreTargetsSync(cfg, {
env,
registeredDatabases
})) {
assertCurrent();
const deletion = deletedAgents.get(target.agentId) ?? "absent";
const databasePath = deletion !== "absent" ? target.storePath : resolveSqliteTargetFromSessionStorePath(target.storePath, {
agentId: target.agentId,
env,
registeredDatabases
}).path;
const retained = deletion === "absent" ? isRetained(databasePath, target.agentId) : void 0;
if (deletion !== "absent" || typeof retained === "object") {
stores.push({
...target,
skippedReason: deletion === "pending" ? "agent-deletion-pending" : "agent-deletion-complete"
});
continue;
}
if (retained) {
const reason = retained === "held" ? "deletion journal reconstruction" : "deletion journal unavailable";
const warning = `Held agent ${target.agentId} database ${databasePath} (${reason}); skipped session retention. Run "${formatCliCommand("openclaw doctor --fix", env)}" for explicit restoration guidance.`;
log$1.warn(warning);
stores.push({
...target,
skippedReason: "agent-store-held",
warning
});
continue;
}
const result = await runSessionRegistryMaintenanceForStore({
...target,
apply: params.apply,
retentionMs: SESSION_REGISTRY_RETENTION_MS,
runningCronJobIds: runningCronJobs.ids,
assertCurrent
});
assertCurrent();
stores.push({
agentId: target.agentId,
storePath: target.storePath,
pruned: result.pruned,
preservedRunning: result.preservedRunning
});
}
return {
retentionMs: SESSION_REGISTRY_RETENTION_MS,
runningCronJobs: runningCronJobs.count,
pruned: stores.reduce((total, store) => total + ("pruned" in store ? store.pruned : 0), 0),
skippedStores: stores.filter((store) => "skippedReason" in store).length,
stores
};
});
}
//#endregion
//#region src/cron/maintenance.ts
const log = createSubsystemLogger("cron/maintenance");
let scope;
let scheduledSweep;
async function sweep(signal) {
let admitted = false;
try {
const preparation = getAgentDatabaseStartupAdmission()?.pendingPreparation;
if (preparation) {
log.info("Cron maintenance deferred until agent database startup preparation completes");
await racePromiseWithAbortSignal(preparation, signal);
}
await runWithGatewayIndependentRootWorkAdmission(async () => {
admitted = true;
const context = captureOpenClawStateWorkerContext();
const assertCurrent = context.admission.assertCurrent;
await maintainCronRunHistory(context, assertCurrent);
assertCurrent();
const result = await runSessionRegistryMaintenance({
apply: true,
assertCurrent
});
assertCurrent();
if (result.skippedReason) log.warn("Session registry maintenance skipped", { reason: result.skippedReason });
}, "cron:maintenance", signal);
} catch (error) {
if (admitted || !signal.aborted && !isGatewayRestartDrainError(error)) log.warn("Cron maintenance failed", { error });
}
}
/** Gateway lifecycle owns retention even when scheduled execution is disabled. */
function startCronMaintenance(scheduler) {
if (scope && !scope.signal.aborted) return;
scope = scheduler.scope();
const { signal } = scope;
scope.schedule({
id: "cron-maintenance",
delayMs: 5e3,
everyMs: 6e4,
run: () => scheduledSweep ??= sweep(signal).finally(() => {
scheduledSweep = void 0;
})
});
}
async function stopCronMaintenance() {
await Promise.all([scope?.stop(), scheduledSweep]);
}
//#endregion
export { startCronMaintenance, stopCronMaintenance };