DeepSeek Harness Plugin

mrzhangkris/dsh-session-pruner

Stars ★ 1 Category Sessions & Messages Added 2026-08-21 npm dsh-session-pruner

Full-type session lifecycle management: one-shot subagents archived on completion, idle continuable subagents / main sessions archived (recoverable), a capacity cap, projection-cache cleanup, and a hot-reload settings panel.

Install

# from npm (prebuilt)

dsh plugin --profile web add dsh-session-pruner

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:mrzhangkris/dsh-session-pruner

Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).

README

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.

简体中文 · Apache-2.0 · 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-seed are 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 accounting

GUI 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-pruner

From source (development)

dsh plugin --profile web add /path/to/dsh-session-pruner

Restart 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 expired

cache=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 path

Implementation notes

  • Multi-frame zstd: DSH session logs are concatenated zstd frames (append writes); Node zlib decodes a single frame only, so the plugin shells out to the system zstd CLI (brew install zstd on 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), onChange re-schedules the timer instantly

Developer guide

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 zstd CLI
  • The root fix lives upstream: projcache stale-session eviction / incremental storage writes, see deepseek-harness Discussion #1550

License

Apache-2.0

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →