feat(FN-1146): add configurable AI session cleanup lifecycle
- Add aiSessionTtlMs and aiSessionCleanupIntervalMs project settings with defaults and bounds for cleanup scheduling - Extend AiSessionStore cleanup to expire stale in-progress sessions, emit deletion events, and support start/stop scheduled cleanup loops - Wire server startup/shutdown to load cleanup settings and manage the scheduled ai_sessions sweep lifecycle - Align planning, subtask breakdown, and mission interview in-memory session retention to a 7-day TTL with shared deletion-driven cleanup - Update schema/tests/docs for migration v15 ai_sessions indexing and end-to-end cleanup/TTL behavior stability
This commit is contained in:
@@ -23,6 +23,14 @@ import { setAiSessionStore as setMissionAiSessionStore } from "./mission-intervi
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const DEFAULT_AI_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const MIN_AI_SESSION_TTL_MS = 10 * 60 * 1000;
|
||||
const MAX_AI_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const DEFAULT_AI_SESSION_CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
|
||||
const MIN_AI_SESSION_CLEANUP_INTERVAL_MS = 60 * 1000;
|
||||
const MAX_AI_SESSION_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export interface ServerOptions {
|
||||
/** Custom merge handler — when provided, used instead of store.mergeTask */
|
||||
onMerge?: (taskId: string) => Promise<MergeResult>;
|
||||
@@ -97,6 +105,18 @@ function normalizeListenArgsForTests(args: unknown[]): unknown[] {
|
||||
return args;
|
||||
}
|
||||
|
||||
function resolveBoundedMs(
|
||||
value: number | undefined,
|
||||
fallback: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
export function createServer(store: TaskStore, options?: ServerOptions): ReturnType<typeof express> {
|
||||
const app = express();
|
||||
const mutationRateLimit = rateLimit(RATE_LIMITS.mutation);
|
||||
@@ -302,6 +322,39 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
setSubtaskAiSessionStore(aiSessionStore);
|
||||
setMissionAiSessionStore(aiSessionStore);
|
||||
|
||||
const loadSettings = (store as { getSettings?: () => Promise<{ aiSessionTtlMs?: number; aiSessionCleanupIntervalMs?: number }> }).getSettings;
|
||||
if (typeof loadSettings === "function") {
|
||||
void loadSettings
|
||||
.call(store)
|
||||
.then((settings) => {
|
||||
const ttlMs = resolveBoundedMs(
|
||||
settings.aiSessionTtlMs,
|
||||
DEFAULT_AI_SESSION_TTL_MS,
|
||||
MIN_AI_SESSION_TTL_MS,
|
||||
MAX_AI_SESSION_TTL_MS,
|
||||
);
|
||||
const cleanupIntervalMs = resolveBoundedMs(
|
||||
settings.aiSessionCleanupIntervalMs,
|
||||
DEFAULT_AI_SESSION_CLEANUP_INTERVAL_MS,
|
||||
MIN_AI_SESSION_CLEANUP_INTERVAL_MS,
|
||||
MAX_AI_SESSION_CLEANUP_INTERVAL_MS,
|
||||
);
|
||||
aiSessionStore.startScheduledCleanup(cleanupIntervalMs, ttlMs);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("[server] Failed to load settings for AI session cleanup; using defaults", err);
|
||||
aiSessionStore.startScheduledCleanup(
|
||||
DEFAULT_AI_SESSION_CLEANUP_INTERVAL_MS,
|
||||
DEFAULT_AI_SESSION_TTL_MS,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
aiSessionStore.startScheduledCleanup(
|
||||
DEFAULT_AI_SESSION_CLEANUP_INTERVAL_MS,
|
||||
DEFAULT_AI_SESSION_TTL_MS,
|
||||
);
|
||||
}
|
||||
|
||||
// REST API
|
||||
app.use("/api", createApiRoutes(store, { ...options, aiSessionStore }));
|
||||
|
||||
@@ -340,6 +393,10 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
const normalizedArgs = normalizeListenArgsForTests(args) as Parameters<typeof originalListen>;
|
||||
const server = originalListen(...normalizedArgs);
|
||||
|
||||
server.once("close", () => {
|
||||
aiSessionStore.stopScheduledCleanup();
|
||||
});
|
||||
|
||||
if (!dashboardApp.__kbWebSocketsAttached) {
|
||||
dashboardApp.__kbWebSocketsAttached = true;
|
||||
setupTerminalWebSocket(dashboardApp, server);
|
||||
|
||||
Reference in New Issue
Block a user