import type { BlobStore } from "./blobs"; import type { Store } from "./store"; /** * Drops the argument fragments a tool call was typed in, once they're past the * window in which a reconnecting client could still want them. * * The log is append-only in the sense that matters — what was said is never * rewritten — and these are not that. They are how a call ARRIVED, kept for the * seconds somebody might be watching it arrive. In a real tool-heavy session * they were 74% of the rows and 61% of the bytes, read by nothing. */ export function sweepInputDeltas(store: Store, ttlMs: number): number { return store.pruneInputDeltas(Date.now() - ttlMs); } /** * Deletes blobs that no conversation references and that are older than the * grace window, reclaiming both the bytes and the metadata row. Returns the * sha256s collected. * * Order matters for crash-safety: the bytes go first, then the row. A crash in * between leaves the row as a retry marker (the next sweep re-collects it), * never a byte with no record. The sweep is idempotent, so running it in one * process (or repeatedly) is safe. * * Until attachments/artifacts populate `blob_refs`, every blob is unreferenced, * so this reclaims stray uploads — e.g. a file staged but whose prompt was * never sent — once they age past the grace window. */ export async function sweepOrphanBlobs( store: Store, blobs: BlobStore, graceMs: number, ): Promise { const olderThan = Date.now() - graceMs; const orphans = store.findOrphanBlobs(olderThan); const collected: string[] = []; for (const sha256 of orphans) { await blobs.delete(sha256); // bytes first… store.deleteBlob(sha256); // …then the row collected.push(sha256); } return collected; }