socraticode
Version:
SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.
94 lines • 4.72 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { collectionName, contextCollectionName, graphCollectionName } from "../config.js";
import { reclaimProjectIdentity } from "../tools/index-tools.js";
import { assertLocalCleanupStore, assertLocalIndexRetired, automaticCleanupEnabled, parseLocalIndexOwnership } from "./local-index-ownership.js";
import { logger } from "./logger.js";
import { getProjectReclamationInventory } from "./qdrant.js";
let timer = null;
let pending = null;
let stopped = true;
async function retirementRefusal(entry) {
if (entry.requiresManualInspection)
return entry.manualInspectionReasons.join("; ");
if (entry.inProgress)
return "a metadata record reports indexing in progress";
if (entry.metadataRecords.length === 0)
return "no local ownership metadata; report-only";
const records = entry.metadataRecords.map((record) => parseLocalIndexOwnership(record.localIndexOwnership));
const ownership = records[0];
if (!ownership || records.some((record) => !record)) {
const refused = entry.metadataRecords.map((record) => record.localIndexOwnership).find((value) => value && typeof value === "object" && "refusal" in value);
return refused ? `ownership refused: ${String(refused.refusal)}; report-only` : "legacy or incomplete local ownership metadata; report-only";
}
if (records.some((record) => JSON.stringify(record) !== JSON.stringify(ownership)))
return "metadata records disagree about local ownership";
if (ownership.identity !== entry.identity || entry.metadataRecords.some((record) => record.projectPath !== ownership.projectPath))
return "the recorded identity/path association changed";
for (const name of [collectionName(entry.identity), graphCollectionName(entry.identity), contextCollectionName(entry.identity)]) {
if (entry.resourceCollections.includes(name) && !entry.metadataRecords.some((record) => record.collectionName === name))
return "an index resource has no ownership metadata; report-only";
}
try {
await assertLocalIndexRetired(ownership);
return null;
}
catch (error) {
return error instanceof Error ? error.message : String(error);
}
}
/** One explicit-local sweep, returning every deletion/refusal instead of treating partial cleanup as success. */
export async function runAutomaticCleanup(mayContinue = () => true) {
if (!automaticCleanupEnabled())
return [];
assertLocalCleanupStore();
const inventory = await getProjectReclamationInventory();
const results = [];
for (const entry of inventory.entries) {
if (!mayContinue())
break;
const refusal = await retirementRefusal(entry);
if (refusal) {
results.push(`Retained identity ${entry.identity}: ${refusal}.`);
continue;
}
results.push(await reclaimProjectIdentity(entry.identity, entry.confirmationToken, async (fresh) => mayContinue() ? retirementRefusal(fresh) : "automatic cleanup stopped for shutdown"));
}
for (const record of inventory.unrecognisedMetadata)
results.push(`Retained unrecognised metadata point ${record.pointId}: ${record.reason}.`);
for (const collection of inventory.unattributedCollections)
results.push(`Retained unattributed collection ${collection.name}: ${collection.reason}.`);
return results;
}
/** Run at MCP startup and every minute, without provisioning Qdrant or overlapping sweeps. */
export function startAutomaticCleanup() {
if (!automaticCleanupEnabled() || timer)
return;
stopped = false;
const sweep = () => {
if (stopped || pending)
return;
pending = runAutomaticCleanup(() => !stopped).then((results) => {
for (const result of results) {
if (result.startsWith("Removed"))
logger.info("Automatic index cleanup", { result });
else
logger.warn("Automatic index cleanup", { result });
}
}).catch((error) => {
logger.error("Automatic index cleanup failed; cleanup is not complete", { error: error instanceof Error ? error.message : String(error) });
}).finally(() => { pending = null; });
};
timer = setInterval(sweep, 60_000);
timer.unref();
sweep();
}
/** Cancel future deletion and drain the current sweep before writer locks are released. */
export async function stopAutomaticCleanup() {
stopped = true;
if (timer)
clearInterval(timer);
timer = null;
await pending;
}
//# sourceMappingURL=auto-cleanup.js.map