Files
openclaw/src/cli/parse-bytes.ts

47 lines
1.1 KiB
TypeScript
Raw Normal View History

fix: unify session maintenance and cron run pruning (#13083) * fix: prune stale session entries, cap entry count, and rotate sessions.json The sessions.json file grows unbounded over time. Every heartbeat tick (default: 30m) triggers multiple full rewrites, and session keys from groups, threads, and DMs accumulate indefinitely with large embedded objects (skillsSnapshot, systemPromptReport). At >50MB the synchronous JSON parse blocks the event loop, causing Telegram webhook timeouts and effectively taking the bot down. Three mitigations, all running inside saveSessionStoreUnlocked() on every write: 1. Prune stale entries: remove entries with updatedAt older than 30 days (configurable via session.maintenance.pruneDays in openclaw.json) 2. Cap entry count: keep only the 500 most recently updated entries (configurable via session.maintenance.maxEntries). Entries without updatedAt are evicted first. 3. File rotation: if the existing sessions.json exceeds 10MB before a write, rename it to sessions.json.bak.{timestamp} and keep only the 3 most recent backups (configurable via session.maintenance.rotateBytes). All three thresholds are configurable under session.maintenance in openclaw.json with Zod validation. No env vars. Existing tests updated to use Date.now() instead of epoch-relative timestamps (1, 2, 3) that would be incorrectly pruned as stale. 27 new tests covering pruning, capping, rotation, and integration scenarios. * feat: auto-prune expired cron run sessions (#12289) Add TTL-based reaper for isolated cron run sessions that accumulate indefinitely in sessions.json. New config option: cron.sessionRetention: string | false (default: '24h') The reaper runs piggy-backed on the cron timer tick, self-throttled to sweep at most every 5 minutes. It removes session entries matching the pattern cron:<jobId>:run:<uuid> whose updatedAt + retention < now. Design follows the Kubernetes ttlSecondsAfterFinished pattern: - Sessions are persisted normally (observability/debugging) - A periodic reaper prunes expired entries - Configurable retention with sensible default - Set to false to disable pruning entirely Files changed: - src/config/types.cron.ts: Add sessionRetention to CronConfig - src/config/zod-schema.ts: Add Zod validation for sessionRetention - src/cron/session-reaper.ts: New reaper module (sweepCronRunSessions) - src/cron/session-reaper.test.ts: 12 tests covering all paths - src/cron/service/state.ts: Add cronConfig/sessionStorePath to deps - src/cron/service/timer.ts: Wire reaper into onTimer tick - src/gateway/server-cron.ts: Pass config and session store path to deps Closes #12289 * fix: sweep cron session stores per agent * docs: add changelog for session maintenance (#13083) (thanks @skyfallsin, @Glucksberg) * fix: add warn-only session maintenance mode * fix: warn-only maintenance defaults to active session * fix: deliver maintenance warnings to active session * docs: add session maintenance examples * fix: accept duration and size maintenance thresholds * refactor: share cron run session key check * fix: format issues and replace defaultRuntime.warn with console.warn --------- Co-authored-by: Pradeep Elankumaran <pradeepe@gmail.com> Co-authored-by: Glucksberg <markuscontasul@gmail.com> Co-authored-by: max <40643627+quotentiroler@users.noreply.github.com> Co-authored-by: quotentiroler <max.nussbaumer@maxhealth.tech>
2026-02-09 23:42:35 -05:00
export type BytesParseOptions = {
defaultUnit?: "b" | "kb" | "mb" | "gb" | "tb";
};
const UNIT_MULTIPLIERS: Record<string, number> = {
b: 1,
kb: 1024,
k: 1024,
mb: 1024 ** 2,
m: 1024 ** 2,
gb: 1024 ** 3,
g: 1024 ** 3,
tb: 1024 ** 4,
t: 1024 ** 4,
};
export function parseByteSize(raw: string, opts?: BytesParseOptions): number {
const trimmed = String(raw ?? "")
.trim()
.toLowerCase();
if (!trimmed) {
throw new Error("invalid byte size (empty)");
}
const m = /^(\d+(?:\.\d+)?)([a-z]+)?$/.exec(trimmed);
if (!m) {
throw new Error(`invalid byte size: ${raw}`);
}
const value = Number(m[1]);
if (!Number.isFinite(value) || value < 0) {
throw new Error(`invalid byte size: ${raw}`);
}
const unit = (m[2] ?? opts?.defaultUnit ?? "b").toLowerCase();
const multiplier = UNIT_MULTIPLIERS[unit];
if (!multiplier) {
throw new Error(`invalid byte size unit: ${raw}`);
}
const bytes = Math.round(value * multiplier);
if (!Number.isFinite(bytes)) {
throw new Error(`invalid byte size: ${raw}`);
}
return bytes;
}