feat(cleanup): hide and prune stale mailboxes

Mailbox listings grew unbounded as old sessions ended without
unregistering. This adds two layers of cleanup, configurable via
mailbox.json or `serve` flags:

- Lazy filter: list responses (REST /v1/list, MCP list_mailboxes)
  drop mailboxes idle longer than hideAfterMinutes (default 24h),
  while always keeping the caller and any sender with messages
  pending for them.
- Background sweep: startServer runs an initial prune on boot and
  schedules an unref'd interval timer that hard-deletes mailboxes
  idle longer than deleteAfterMinutes (default 7d) which have no
  pending messages, and wipes their delivered history.
This commit is contained in:
Mika Kuns
2026-05-20 13:54:03 +02:00
parent 06a2ea6b7b
commit 0c06e2cf4b
7 changed files with 349 additions and 17 deletions

View File

@@ -4,17 +4,26 @@ import { join, resolve } from "node:path";
export const DEFAULT_PORT = 37849;
export const DEFAULT_BIND = "127.0.0.1";
export const DEFAULT_HIDE_AFTER_MINUTES = 60 * 24;
export const DEFAULT_DELETE_AFTER_MINUTES = 60 * 24 * 7;
export const DEFAULT_SWEEP_INTERVAL_MINUTES = 60;
export interface FileConfig {
port?: number;
bind?: string;
dbPath?: string;
hideAfterMinutes?: number;
deleteAfterMinutes?: number;
sweepIntervalMinutes?: number;
}
export interface DaemonConfig {
port: number;
bind: string;
dbPath: string;
hideAfterMinutes: number;
deleteAfterMinutes: number;
sweepIntervalMinutes: number;
}
export function defaultDbPath(): string {
@@ -65,6 +74,12 @@ export function loadFileConfig(explicitPath?: string): FileConfig {
port: typeof parsed.port === "number" ? parsed.port : undefined,
bind: typeof parsed.bind === "string" ? parsed.bind : undefined,
dbPath: typeof parsed.dbPath === "string" ? parsed.dbPath : undefined,
hideAfterMinutes:
typeof parsed.hideAfterMinutes === "number" ? parsed.hideAfterMinutes : undefined,
deleteAfterMinutes:
typeof parsed.deleteAfterMinutes === "number" ? parsed.deleteAfterMinutes : undefined,
sweepIntervalMinutes:
typeof parsed.sweepIntervalMinutes === "number" ? parsed.sweepIntervalMinutes : undefined,
};
}
}
@@ -76,6 +91,9 @@ export interface ServeOverrides {
bind?: string;
dbPath?: string;
config?: string;
hideAfterMinutes?: number;
deleteAfterMinutes?: number;
sweepIntervalMinutes?: number;
}
export function resolveConfig(overrides: ServeOverrides): DaemonConfig {
@@ -83,7 +101,20 @@ export function resolveConfig(overrides: ServeOverrides): DaemonConfig {
const port = overrides.port ?? file.port ?? DEFAULT_PORT;
const bind = overrides.bind ?? file.bind ?? DEFAULT_BIND;
const dbPathRaw = overrides.dbPath ?? file.dbPath ?? defaultDbPath();
return { port, bind, dbPath: expandPath(dbPathRaw) };
const hideAfterMinutes =
overrides.hideAfterMinutes ?? file.hideAfterMinutes ?? DEFAULT_HIDE_AFTER_MINUTES;
const deleteAfterMinutes =
overrides.deleteAfterMinutes ?? file.deleteAfterMinutes ?? DEFAULT_DELETE_AFTER_MINUTES;
const sweepIntervalMinutes =
overrides.sweepIntervalMinutes ?? file.sweepIntervalMinutes ?? DEFAULT_SWEEP_INTERVAL_MINUTES;
return {
port,
bind,
dbPath: expandPath(dbPathRaw),
hideAfterMinutes,
deleteAfterMinutes,
sweepIntervalMinutes,
};
}
export function baseUrl(cfg: { port: number; bind: string }): string {