Add stale RAG provider cleanup
This commit is contained in:
+1
-1
@@ -136,7 +136,7 @@
|
|||||||
## 10. Operational cleanup and observability
|
## 10. Operational cleanup and observability
|
||||||
|
|
||||||
- [x] Add retention policy for `data/cache/internal-artifacts`.
|
- [x] Add retention policy for `data/cache/internal-artifacts`.
|
||||||
- [ ] Add retention policy for stale RAG vector/library provider state.
|
- [x] Add retention policy for stale RAG vector/library provider state.
|
||||||
- [x] Add command or admin view for recent `ai_requests`.
|
- [x] Add command or admin view for recent `ai_requests`.
|
||||||
- [x] Add command or admin view for request audit by message id.
|
- [x] Add command or admin view for request audit by message id.
|
||||||
- [x] Add command to inspect artifacts for a message.
|
- [x] Add command to inspect artifacts for a message.
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import type {RagArtifactPayload} from "./rag-artifact-payload";
|
||||||
|
|
||||||
|
export type ArtifactLike = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
payload: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RagCleanupTarget = {
|
||||||
|
artifactId: string;
|
||||||
|
createdAt: string;
|
||||||
|
provider: RagArtifactPayload["providerState"]["provider"];
|
||||||
|
vectorStoreIds?: string[];
|
||||||
|
uploadedFileIds?: string[];
|
||||||
|
libraryId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RagCleanupPlan = {
|
||||||
|
cutoffAt: string;
|
||||||
|
targets: RagCleanupTarget[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseRagArtifactPayload(payload: string): RagArtifactPayload | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(payload) as Partial<RagArtifactPayload>;
|
||||||
|
if (!parsed || parsed.artifactKind !== "rag" || !parsed.providerState) return null;
|
||||||
|
return parsed as RagArtifactPayload;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStaleRagCleanupPlan(
|
||||||
|
artifacts: ArtifactLike[],
|
||||||
|
retentionDays = 14,
|
||||||
|
now = new Date(),
|
||||||
|
): RagCleanupPlan {
|
||||||
|
const cutoffAt = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
const targets: RagCleanupTarget[] = [];
|
||||||
|
|
||||||
|
for (const artifact of artifacts) {
|
||||||
|
if (artifact.createdAt > cutoffAt) continue;
|
||||||
|
|
||||||
|
const payload = parseRagArtifactPayload(artifact.payload);
|
||||||
|
if (!payload || payload.artifactKind !== "rag") continue;
|
||||||
|
|
||||||
|
switch (payload.providerState.provider) {
|
||||||
|
case "OPENAI":
|
||||||
|
if (payload.providerState.vectorStoreIds.length || payload.providerState.uploadedFileIds.length) {
|
||||||
|
targets.push({
|
||||||
|
artifactId: artifact.id,
|
||||||
|
createdAt: artifact.createdAt,
|
||||||
|
provider: payload.providerState.provider,
|
||||||
|
vectorStoreIds: [...payload.providerState.vectorStoreIds],
|
||||||
|
uploadedFileIds: [...payload.providerState.uploadedFileIds],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "MISTRAL":
|
||||||
|
if (payload.providerState.libraryId) {
|
||||||
|
targets.push({
|
||||||
|
artifactId: artifact.id,
|
||||||
|
createdAt: artifact.createdAt,
|
||||||
|
provider: payload.providerState.provider,
|
||||||
|
libraryId: payload.providerState.libraryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "OLLAMA":
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {cutoffAt, targets};
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import {appLogger} from "../logging/logger.js";
|
||||||
|
import {DatabaseManager} from "../db/database-manager.js";
|
||||||
|
import {AiProvider} from "../model/ai-provider.js";
|
||||||
|
import {createOpenAiClient, resolveAiRuntimeTarget} from "./ai-runtime-target.js";
|
||||||
|
import {deleteMistralLibrary} from "./unified-ai-runner.shared.js";
|
||||||
|
import {buildStaleRagCleanupPlan} from "./rag-retention-planner.js";
|
||||||
|
|
||||||
|
const logger = appLogger.child("rag-retention");
|
||||||
|
|
||||||
|
function unique(values: string[]): string[] {
|
||||||
|
return [...new Set(values.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupOpenAiRag(vectorStoreIds: string[], uploadedFileIds: string[]): Promise<void> {
|
||||||
|
const target = resolveAiRuntimeTarget(AiProvider.OPENAI, "documents");
|
||||||
|
const client = createOpenAiClient(target);
|
||||||
|
|
||||||
|
for (const vectorStoreId of unique(vectorStoreIds)) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
logger.info("openai.vector_store.cleanup.start", {vectorStoreId});
|
||||||
|
try {
|
||||||
|
await client.vectorStores.delete(vectorStoreId);
|
||||||
|
logger.success("openai.vector_store.cleanup.done", {vectorStoreId, duration: `${Date.now() - startedAt}ms`});
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("openai.vector_store.cleanup.failed", {
|
||||||
|
vectorStoreId,
|
||||||
|
duration: `${Date.now() - startedAt}ms`,
|
||||||
|
error: error instanceof Error ? error : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fileId of unique(uploadedFileIds)) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
logger.info("openai.file.cleanup.start", {fileId});
|
||||||
|
try {
|
||||||
|
await client.files.delete(fileId);
|
||||||
|
logger.success("openai.file.cleanup.done", {fileId, duration: `${Date.now() - startedAt}ms`});
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("openai.file.cleanup.failed", {
|
||||||
|
fileId,
|
||||||
|
duration: `${Date.now() - startedAt}ms`,
|
||||||
|
error: error instanceof Error ? error : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupMistralRag(libraryId: string): Promise<void> {
|
||||||
|
const target = resolveAiRuntimeTarget(AiProvider.MISTRAL, "documents");
|
||||||
|
const startedAt = Date.now();
|
||||||
|
logger.info("mistral.library.cleanup.start", {libraryId});
|
||||||
|
try {
|
||||||
|
await deleteMistralLibrary(libraryId, target);
|
||||||
|
logger.success("mistral.library.cleanup.done", {libraryId, duration: `${Date.now() - startedAt}ms`});
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("mistral.library.cleanup.failed", {
|
||||||
|
libraryId,
|
||||||
|
duration: `${Date.now() - startedAt}ms`,
|
||||||
|
error: error instanceof Error ? error : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupStaleRagProviderState(retentionDays = 14): Promise<{
|
||||||
|
scannedArtifacts: number;
|
||||||
|
cleanupTargets: number;
|
||||||
|
openaiTargets: number;
|
||||||
|
mistralTargets: number;
|
||||||
|
}> {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const artifacts = await DatabaseManager.getAllArtifacts().catch(() => []);
|
||||||
|
const plan = buildStaleRagCleanupPlan(artifacts, retentionDays);
|
||||||
|
|
||||||
|
logger.info("cleanup.start", {
|
||||||
|
retentionDays,
|
||||||
|
scannedArtifacts: artifacts.length,
|
||||||
|
cleanupTargets: plan.targets.length,
|
||||||
|
cutoffAt: plan.cutoffAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
let openaiTargets = 0;
|
||||||
|
let mistralTargets = 0;
|
||||||
|
|
||||||
|
for (const target of plan.targets) {
|
||||||
|
switch (target.provider) {
|
||||||
|
case "OPENAI":
|
||||||
|
openaiTargets += 1;
|
||||||
|
await cleanupOpenAiRag(target.vectorStoreIds ?? [], target.uploadedFileIds ?? []);
|
||||||
|
break;
|
||||||
|
case "MISTRAL":
|
||||||
|
mistralTargets += 1;
|
||||||
|
if (target.libraryId) {
|
||||||
|
await cleanupMistralRag(target.libraryId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "OLLAMA":
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success("cleanup.done", {
|
||||||
|
retentionDays,
|
||||||
|
scannedArtifacts: artifacts.length,
|
||||||
|
cleanupTargets: plan.targets.length,
|
||||||
|
openaiTargets,
|
||||||
|
mistralTargets,
|
||||||
|
duration: `${Date.now() - startedAt}ms`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scannedArtifacts: artifacts.length,
|
||||||
|
cleanupTargets: plan.targets.length,
|
||||||
|
openaiTargets,
|
||||||
|
mistralTargets,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ import {cleanupInternalArtifactCache} from "./ai/internal-artifact-store.js";
|
|||||||
import {AIAudit} from "./commands/ai-audit.js";
|
import {AIAudit} from "./commands/ai-audit.js";
|
||||||
import {AIMetrics} from "./commands/ai-metrics.js";
|
import {AIMetrics} from "./commands/ai-metrics.js";
|
||||||
import {AIRequests} from "./commands/ai-requests.js";
|
import {AIRequests} from "./commands/ai-requests.js";
|
||||||
|
import {cleanupStaleRagProviderState} from "./ai/rag-retention.js";
|
||||||
|
|
||||||
process.setUncaughtExceptionCaptureCallback(logError);
|
process.setUncaughtExceptionCaptureCallback(logError);
|
||||||
|
|
||||||
@@ -278,6 +279,7 @@ async function main() {
|
|||||||
}, () => ({notesRootFilePath}));
|
}, () => ({notesRootFilePath}));
|
||||||
|
|
||||||
await measureStartupStep("cleanup_internal_artifacts", () => cleanupInternalArtifactCache(), () => ({retentionDays: 14}));
|
await measureStartupStep("cleanup_internal_artifacts", () => cleanupInternalArtifactCache(), () => ({retentionDays: 14}));
|
||||||
|
await measureStartupStep("cleanup_stale_rag_provider_state", () => cleanupStaleRagProviderState(), () => ({retentionDays: 14}));
|
||||||
await measureStartupStep("observability.snapshot", async () => {
|
await measureStartupStep("observability.snapshot", async () => {
|
||||||
const [aiRequests, attachments, artifacts, requestAudits] = await Promise.all([
|
const [aiRequests, attachments, artifacts, requestAudits] = await Promise.all([
|
||||||
DatabaseManager.getAllAiRequests(),
|
DatabaseManager.getAllAiRequests(),
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
const {buildStaleRagCleanupPlan} = await import("../dist/ai/rag-retention-planner.js");
|
||||||
|
|
||||||
|
test("stale rag cleanup plan selects only older rag artifacts", () => {
|
||||||
|
const plan = buildStaleRagCleanupPlan([
|
||||||
|
{
|
||||||
|
id: "recent-openai",
|
||||||
|
createdAt: "2026-05-18T00:00:00.000Z",
|
||||||
|
payload: JSON.stringify({
|
||||||
|
artifactKind: "rag",
|
||||||
|
providerState: {
|
||||||
|
provider: "OPENAI",
|
||||||
|
vectorStoreIds: ["vs_1"],
|
||||||
|
uploadedFileIds: ["file_1"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stale-openai",
|
||||||
|
createdAt: "2026-04-01T00:00:00.000Z",
|
||||||
|
payload: JSON.stringify({
|
||||||
|
artifactKind: "rag",
|
||||||
|
providerState: {
|
||||||
|
provider: "OPENAI",
|
||||||
|
vectorStoreIds: ["vs_2"],
|
||||||
|
uploadedFileIds: ["file_2"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stale-ollama",
|
||||||
|
createdAt: "2026-04-01T00:00:00.000Z",
|
||||||
|
payload: JSON.stringify({
|
||||||
|
artifactKind: "rag",
|
||||||
|
providerState: {
|
||||||
|
provider: "OLLAMA",
|
||||||
|
prepared: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
], 14, new Date("2026-05-18T00:00:00.000Z"));
|
||||||
|
|
||||||
|
assert.equal(plan.targets.length, 1);
|
||||||
|
assert.deepEqual(plan.targets[0], {
|
||||||
|
artifactId: "stale-openai",
|
||||||
|
createdAt: "2026-04-01T00:00:00.000Z",
|
||||||
|
provider: "OPENAI",
|
||||||
|
vectorStoreIds: ["vs_2"],
|
||||||
|
uploadedFileIds: ["file_2"],
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user