fix(sessions): archive transcript files when pruning stale entries

pruneStaleEntries() removed entries from sessions.json but left the
corresponding .jsonl transcript files on disk indefinitely.

Added an onPruned callback to collect pruned session IDs, then
archives their transcript files via archiveSessionTranscripts()
after pruning completes. Only runs in enforce mode.
This commit is contained in:
Hudson
2026-02-16 15:23:07 -05:00
committed by Peter Steinberger
parent 441401221d
commit 93fbe6482b
2 changed files with 57 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ import type { SessionMaintenanceConfig, SessionMaintenanceMode } from "../types.
import { acquireSessionWriteLock } from "../../agents/session-write-lock.js";
import { parseByteSize } from "../../cli/parse-bytes.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import { archiveSessionTranscripts } from "../../gateway/session-utils.fs.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
deliveryContextFromSession,
@@ -301,13 +302,14 @@ export function resolveMaintenanceConfig(): ResolvedSessionMaintenanceConfig {
export function pruneStaleEntries(
store: Record<string, SessionEntry>,
overrideMaxAgeMs?: number,
opts: { log?: boolean } = {},
opts: { log?: boolean; onPruned?: (params: { key: string; entry: SessionEntry }) => void } = {},
): number {
const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfig().pruneAfterMs;
const cutoffMs = Date.now() - maxAgeMs;
let pruned = 0;
for (const [key, entry] of Object.entries(store)) {
if (entry?.updatedAt != null && entry.updatedAt < cutoffMs) {
opts.onPruned?.({ key, entry });
delete store[key];
pruned++;
}
@@ -510,8 +512,23 @@ async function saveSessionStoreUnlocked(
}
} else {
// Prune stale entries and cap total count before serializing.
pruneStaleEntries(store, maintenance.pruneAfterMs);
const prunedSessionFiles = new Map<string, string | undefined>();
pruneStaleEntries(store, maintenance.pruneAfterMs, {
onPruned: ({ entry }) => {
if (!prunedSessionFiles.has(entry.sessionId) || entry.sessionFile) {
prunedSessionFiles.set(entry.sessionId, entry.sessionFile);
}
},
});
capEntryCount(store, maintenance.maxEntries);
for (const [sessionId, sessionFile] of prunedSessionFiles) {
archiveSessionTranscripts({
sessionId,
storePath,
sessionFile,
reason: "deleted",
});
}
// Rotate the on-disk file if it exceeds the size threshold.
await rotateSessionFile(storePath, maintenance.rotateBytes);