dsh-session-pruner
DSH session lifecycle management plugin — full-type session lifecycle management: one-shot subagents archived on completion, continuable subagents and main sessions archived when idle, a capacity cap, and projection-cache cleanup. Prevents session-library accumulation stalls at the source.
> Every session type has a defined destination: finished one-shot subagents are archived automatically, idle continuable subagents / main sessions are archived, and overflow is recycled by priority. Archive first (recoverable), delete after expiry — the GUI syncs within 30s, fully panel-configured with hot reload.
[简体中文](README.zh-CN.md) · [Apache-2.0](LICENSE) · npm
Why
DSH (DeepSeek Harness) caches a full projection of every session in session_projcache.json (token stats, context pressure, ...), and the storage backend rewrites the whole file atomically on every write. When the session library accumulates thousands of subagent sessions:
- The cache balloons past 100MB and each checkpoint fully re-serializes → main process CPU 250%+
- The single-threaded event loop is saturated → every session load stalls, even
GET /times out
Managing session lifecycle (this plugin) is the root fix: no session accumulation → no cache rows → no stalls.
Features: full-type lifecycle
| Session type | Trigger | Action | Default |
|---|---|---|---|
| one-shot subagent | log contains session/end-seed (finished) | archive/delete at next scan | 30min interval |
| continuable subagent | idle over N days | archive (recoverable) | off (0 days) |
| main session | idle over N days | archive (recoverable) | off (0 days) |
| any type | total exceeds capacity cap | recycle by one-shot → continuable → main + oldest | 400 |
| archive directory | kept over N hours | physically deleted | 24 hours |
Archive mechanism (recoverable)
Cleaned sessions are moved to ~/.dsh/sessions-archive/ first (workspace/session-id structure preserved) — they disappear from the GUI immediately (the list only reads the sessions directory), but the files remain and can be restored manually:
# Restore: mv back into the sessions directory
mv ~/.dsh/sessions-archive/<workspace>/<session-id> ~/.dsh/sessions/<workspace>/A "delete directly" mode (no archive, irreversible) is also available.
Safety (double protection)
- Running sessions are never touched: logs without
session/end-seedare never cleaned (checked in both the one-shot path and the capacity cap) - live protection: sessions still held in the in-memory session store (open/loading) are skipped
- Main sessions do not participate in capacity recycling by default (configurable)
- Per-action failure isolation: every action is try/catch wrapped
How it works
scan (scheduled, default 30min)
├─ pruneArchive: physically delete expired archive sessions
├─ iterate ~/.dsh/sessions/*/ decompress log (system zstd, multi-frame)
│ ├─ origin: main | subagent (session header)
│ ├─ mode: one-shot | continuable (subagent/descriptor event)
│ └─ ended: contains session/end-seed
├─ one-shot + ended ──→ archive (archiveMode)
├─ continuable/main idle N days ──→ archive
├─ total > cap ──→ recycle by priority + oldest (skip running/live)
└─ each archive also: purge projcache row + workspace accountingGUI sync: the client calls sessions.refreshList() every uiRefreshSeconds seconds, so cleaned sessions disappear from the sidebar automatically — no page reload needed.
Install
From npm (recommended)
dsh plugin --profile web add dsh-session-prunerFrom source (development)
dsh plugin --profile web add /path/to/dsh-session-prunerRestart dsh web after install (launchctl kickstart -k gui/$(id -u)/com.deepseek.dsh-web).
Configuration (settings panel, hot reload)
After install, open Settings → Plugins → 会话生命周期管理 card. All 9 options save with hot reload (no restart):
| Field | Default | Description |
|---|---|---|
| Scan interval (min) | 30 | cleanup loop period |
| Capacity cap (sessions) | 400 | recycle by priority + oldest when exceeded |
| UI refresh interval (s) | 30 | GUI session list refresh period |
| Archive retention (hours) | 24 | physical delete after retention |
| Archive mode | archive | archive (recoverable) / delete directly (irreversible) |
| Continuable idle archive (days) | 0 | archive after N idle days, 0 = off |
| Main idle archive (days) | 0 | archive after N idle days, 0 = off |
| Clean main on overflow | off | main participates in capacity recycling |
| One-shot min survival (min) | 3 | newly finished subagents are not cleaned within N minutes (protects finishing/references) |
Env vars (fallback, panel wins): DSH_SESSION_LIFECYCLE_INTERVAL_MS / _MAX / _CLEAN_MAIN / _ARCHIVE_HOURS / _ARCHIVE_MODE / _CONTINUABLE_IDLE_DAYS / _MAIN_IDLE_DAYS / _ONE_SHOT_MIN_AGE_MINUTES.
Logs
Output in guard server-*.out.log:
[session-lifecycle] armed: interval=30min cap=100 cleanMain=false
[session-lifecycle] hot-reloaded: interval=30min cap=100 ... contIdle=1d mainIdle=2d
[session-lifecycle] archived a1b2c3d4 (subagent/one-shot) one-shot done cache=true
[session-lifecycle] archive pruned: 2 expiredcache=true/false tells whether the projection cache row was purged along with the session.
Tests
node test/dry-run.js # read-only full-library scan, verify classification (no deletion)
node test/e2e.js # create a fake one-shot session, verify the real cleanup pathImplementation notes
- Multi-frame zstd: DSH session logs are concatenated zstd frames (append writes); Node
zlibdecodes a single frame only, so the plugin shells out to the systemzstdCLI (brew install zstdon macOS) - Cache row purge:
storageDomain.get('session_projcache').table('sessions').delete(id)— the official write chain (atomic persistence + in-memory sync) - Workspace accounting: the session id is removed from the workspace domain on archive, keeping the data source consistent with disk
- Zero npm deps: plain Node built-ins + cordis runtime injection
- Panel + hot reload:
installSettingsSection+ hand-written client card (__ModuleLoader__bundle),onChangere-schedules the timer instantly
Developer guide
[docs/DEVELOPMENT-GUIDE.md](docs/DEVELOPMENT-GUIDE.md) — DSH plugin development practice guide (architecture, Host/Client, settings panel, deployment ops, 10 pitfalls with fixes), the foundation for future plugin work.
Known limits
- A finished one-shot subagent survives at most one scan interval
- Requires the system
zstdCLI - The root fix lives upstream: projcache stale-session eviction / incremental storage writes, see deepseek-harness Discussion #1550
License
[Apache-2.0](LICENSE)