feat(sessions): add prune command to delete closed sessions#227
feat(sessions): add prune command to delete closed sessions#227coder999999999 wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Adds `acpx sessions prune` to delete closed sessions and free disk space. Supports filtering by agent, date cutoff, and age threshold, with a dry-run mode to preview what would be removed. Flags: --dry-run Preview what would be pruned without deleting --before <date> Prune sessions closed before a given date --older-than <n> Prune sessions closed more than N days ago --include-history Also delete event stream files (.stream.ndjson) When both --before and --older-than are supplied, --before takes precedence. Output is available in text, json, and quiet formats. Co-authored-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new acpx sessions prune CLI subcommand and underlying persistence support to delete closed session artifacts (and optionally their event streams) to reclaim disk space.
Changes:
- Implements
pruneSessions()in session persistence with age filtering,--dry-run, and optional history deletion. - Adds CLI flag parsing/registration and a command handler that wires flags into
pruneSessions(). - Adds output rendering for
text/json/quietformats and introduces a dedicated test suite for pruning behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/sessions-prune.test.ts | Adds tests covering prune behavior, filters, dry-run, and history deletion. |
| src/session/session.ts | Re-exports pruneSessions and its types from the session module. |
| src/session/persistence/repository.ts | Implements pruneSessions() and related option/result types. |
| src/session/persistence.ts | Re-exports pruneSessions and its types from persistence entrypoint. |
| src/cli/output/render.ts | Adds printPruneResultByFormat() and byte formatting for prune output. |
| src/cli/flags.ts | Adds SessionsPruneFlags and parsers for --before and --older-than. |
| src/cli/command-registration.ts | Registers sessions prune subcommand and its options. |
| src/cli/command-handlers.ts | Adds handleSessionsPrune to execute prune and render results. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const records: SessionRecord[] = []; | ||
| for (const entry of eligible) { | ||
| const record = await loadRecordFromIndexEntry(entry); | ||
| if (record) { |
There was a problem hiding this comment.
pruneSessions determines eligibility from the cached index entry (entry.closed) but does not re-check the loaded record before deleting. If index.json is stale/corrupt and marks an active session as closed, this can delete an open session file. Consider filtering again on record.closed === true (and optionally record.pid == null) after loadRecordFromIndexEntry before adding it to records / deleting.
| if (record) { | |
| if (record?.closed === true) { |
| try { | ||
| const stat = await fs.stat(jsonFile); | ||
| bytesFreed += stat.size; | ||
| } catch { | ||
| // file already gone | ||
| } | ||
| await fs.unlink(jsonFile).catch(() => undefined); | ||
|
|
There was a problem hiding this comment.
bytesFreed is incremented based on stat.size before deletion, but unlink failures are swallowed. If unlink fails (permissions, transient FS errors), the command will report bytes freed even though the file remains, and still include the record in pruned. Consider only adding to bytesFreed after a successful delete and surfacing/returning deletion failures so the CLI output is accurate.
| if (options.includeHistory) { | ||
| const prefix = `${safeId}.stream`; | ||
| for (const name of dirEntries) { | ||
| if (!name.startsWith(prefix)) { | ||
| continue; | ||
| } | ||
| const filePath = path.join(sessionDir, name); | ||
| try { | ||
| const stat = await fs.stat(filePath); | ||
| bytesFreed += stat.size; | ||
| } catch { | ||
| // ignore | ||
| } | ||
| await fs.unlink(filePath).catch(() => undefined); | ||
| } |
There was a problem hiding this comment.
When --include-history is set, this loops over the full dirEntries list for every pruned session, making deletion O(prunedSessions × filesInDir). With many sessions/stream segments this can get slow. Consider pre-indexing dirEntries by safeId/prefix once (or filtering to matching names and removing them from a Set as you delete) to avoid repeated full scans.
| .option("--before <date>", "Prune sessions closed before this date", parsePruneBeforeDate) | ||
| .option("--older-than <days>", "Prune sessions closed more than N days ago", parseDaysOlderThan) |
There was a problem hiding this comment.
The help text says "Prune sessions closed before this date" / "closed more than N days ago", but pruning is implemented using lastUsedAt (not closedAt). This is user-facing behavior documentation; either update the descriptions to match (e.g., "last used before"), or change the pruning logic to use closedAt if that's the intended cutoff.
| .option("--before <date>", "Prune sessions closed before this date", parsePruneBeforeDate) | |
| .option("--older-than <days>", "Prune sessions closed more than N days ago", parseDaysOlderThan) | |
| .option("--before <date>", "Prune sessions last used before this date", parsePruneBeforeDate) | |
| .option("--older-than <days>", "Prune sessions last used more than N days ago", parseDaysOlderThan) |
Summary
acpx sessions pruneto delete closed sessions and free disk space--dry-runto preview what would be removed without deleting anything--before <date>and--older-than <days>to filter by age (--beforetakes precedence if both are supplied)--include-historyto also delete event stream files (.stream.ndjson)text,json, andquietformatsChanges
src/session/persistence/repository.ts— corepruneSessionsfunction; filters closed sessions from the index, deletes their JSON records, optionally deletes stream files, then rebuilds the indexsrc/cli/flags.ts—SessionsPruneFlagstype and parsers for--before(returnsDate) and--older-thansrc/cli/command-handlers.ts—handleSessionsPrunehandlersrc/cli/command-registration.ts— registers theprunesubcommand undersessionssrc/cli/output/render.ts—printPruneResultByFormatwith text, json, and quiet outputtest/sessions-prune.test.ts— 8 tests covering all flag combinations and edge casesTest plan
sessions prune --dry-runlists sessions without deletingsessions prunedeletes closed sessions and reports bytes freedsessions prune --before 2026-01-01respects date cutoffsessions prune --older-than 30respects day thresholdsessions prune --include-historyremoves stream files toosessions prune --format jsonemits valid JSON