openclaw/src/gateway/hooks-session-cleanup.ts
Victor Lassance 8f5a82205c feat(hooks): add TTL-based cleanup for hook sessions
Hook sessions (e.g., from Gmail hooks) now auto-cleanup after a configurable TTL.

Changes:
- Add `hooks.sessionTtlMs` config option (default: 24 hours)
- Add `cleanupStaleHookSessions()` function that deletes stale hook sessions
- Run cleanup hourly via existing maintenance interval
- Only affects sessions with keys starting with "hook:"
- Sessions + transcripts are deleted after TTL expires

Config example:
```yaml
hooks:
  sessionTtlMs: 86400000  # 24 hours (default)
  # Set to 0 to disable cleanup
```

This prevents hook sessions from accumulating indefinitely while still
allowing time for debugging (sessions are kept for 24h by default).
2026-01-29 18:13:54 +08:00

84 lines
2.4 KiB
TypeScript

/**
* TTL-based cleanup for hook sessions.
* Runs periodically to delete stale hook sessions older than the configured TTL.
*/
import type { MoltbotConfig } from "../config/config.js";
import { loadCombinedSessionStoreForGateway, listSessionsFromStore } from "./session-utils.js";
import { callGateway } from "./call.js";
import type { createSubsystemLogger } from "../logging/subsystem.js";
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
const DEFAULT_HOOK_SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
const HOOK_SESSION_PREFIX = "hook:";
export type HookSessionCleanupResult = {
checked: number;
deleted: number;
errors: number;
};
/**
* Clean up stale hook sessions older than TTL.
* Returns counts of checked/deleted/errored sessions.
*/
export async function cleanupStaleHookSessions(params: {
cfg: MoltbotConfig;
log?: SubsystemLogger;
}): Promise<HookSessionCleanupResult> {
const { cfg, log } = params;
// Get TTL from config (0 = disabled)
const ttlMs = cfg.hooks?.sessionTtlMs ?? DEFAULT_HOOK_SESSION_TTL_MS;
if (ttlMs <= 0) {
return { checked: 0, deleted: 0, errors: 0 };
}
const cutoffMs = Date.now() - ttlMs;
// List all sessions
const { storePath, store } = loadCombinedSessionStoreForGateway(cfg);
const allSessions = listSessionsFromStore({
cfg,
storePath,
store,
opts: { limit: 10000 }, // High limit to get all sessions
});
// Filter to hook sessions that are stale
const staleSessions = allSessions.sessions.filter((session) => {
// Check if it's a hook session (key starts with "hook:")
if (!session.key.startsWith(HOOK_SESSION_PREFIX)) return false;
// Check if it's older than TTL
const lastActivity = session.updatedAt ?? 0;
return lastActivity < cutoffMs;
});
let deleted = 0;
let errors = 0;
for (const session of staleSessions) {
try {
await callGateway({
method: "sessions.delete",
params: { key: session.key, deleteTranscript: true },
timeoutMs: 10_000,
});
deleted++;
log?.debug?.(`cleaned up stale hook session: ${session.key}`);
} catch (err) {
errors++;
log?.warn?.(`failed to cleanup hook session ${session.key}: ${String(err)}`);
}
}
if (deleted > 0 || errors > 0) {
log?.info?.(
`hook session cleanup: checked=${staleSessions.length} deleted=${deleted} errors=${errors}`,
);
}
return { checked: staleSessions.length, deleted, errors };
}