using AIStudio.Tools.Databases.ProvenanceStore; namespace AIStudio.Chat; /// /// Removes from the provenance store what no stored chat refers to anymore. /// /// /// Deleting a chat takes its entries along right away, see ChatProvenance, and so does the /// maintenance of the temporary chats. What is left over comes from changes inside a chat, or from /// runs which ended before they could clean up: /// /// a message which was removed, edited, or answered anew, together with its passages and attached documents; /// a text which a preview stored, while its chat was never stored with it, or sending could not remove it; /// an assistant whose conversation was never sent to a chat; /// a chat which was never stored, or deleted while the store was not available. /// /// Each chat file is read for its references, which only a cleanup at the start can do safely: /// while the app runs, a chat in memory may refer to an entry its file does not know yet. For the /// same reason, only entries stored before the start are removed.

/// /// Reading every chat at every start would cost much on a small computer, so a chat is read only /// when something changed since the last complete cleanup: its file was written since, or it /// received an entry since. Otherwise, the last cleanup removed all it could, and nothing it kept /// has become unreferenced. A chat which is not read keeps its entries; skipping never removes /// anything. Chats which no longer exist cost no reading and are always taken care of. ///
public static class ProvenanceCleanup { /// /// How long before the last cleanup a chat file may have been written and still counts as changed. /// /// /// The time of a file may be coarse, e.g., two seconds on FAT, or come from the clock of another /// computer, for a data folder on a network drive. Reading a few chats too many costs little, /// while missing one leaves what it no longer refers to until it changes again. /// private static readonly TimeSpan WRITE_TIME_TOLERANCE = TimeSpan.FromMinutes(5); /// /// Removes the entries of chats which no longer exist, and those their chat no longer refers to. /// /// The provenance store. /// The workspace of every chat on the disk by its ID, Guid.Empty for a temporary chat. /// Reads a chat by its workspace and ID, or answers null when it cannot be read. /// Tells when a chat file was last written, by its workspace and ID, or answers null when that is not known. /// The start of this run: entries stored since may belong to a chat which is not on the disk yet. /// When the last complete cleanup began, or null to read every chat. /// The cancellation token. /// What the cleanup did. public static async Task CleanUpAsync(ProvenanceStoreClient store, IReadOnlyDictionary chatLocations, Func> loadChat, Func getChatWrittenAt, DateTimeOffset createdBefore, DateTimeOffset? lastCleanup, CancellationToken token) { // The chats which received an entry since the last cleanup, in one query, which the last cleanup had to leave alone: var chatsWithNewEntries = lastCleanup is { } since ? (await store.GetChatIdsAsync(token, since)).ToHashSet() : []; var unreferenced = 0; var skipped = 0; var unreadable = 0; var orphanedChats = new List(); foreach (var chatId in await store.GetChatIdsAsync(token)) { token.ThrowIfCancellationRequested(); if (!chatLocations.TryGetValue(chatId, out var workspaceId)) { orphanedChats.Add(chatId); continue; } // A chat whose time of writing is not known counts as changed: if (lastCleanup is { } previousStart && !chatsWithNewEntries.Contains(chatId) && getChatWrittenAt(workspaceId, chatId) is { } writtenAt && writtenAt < previousStart - WRITE_TIME_TOLERANCE) { skipped++; continue; } // A chat which cannot be read keeps everything: nothing tells what it refers to. var chat = await loadChat(workspaceId, chatId); if (chat is null) { unreadable++; continue; } unreferenced += await store.DeleteChatEntriesExceptAsync(chatId, chat.CollectProvenanceIds(), token, createdBefore); } // The chats which no longer exist all at once, since there may be many of them after a long time without a start: var orphaned = await store.DeleteChatsAsync(orphanedChats, token, createdBefore); return new(orphaned, unreferenced, skipped, unreadable); } }