# Archived Sessions · archive-manager > A real "archive center" for **DeepSeek Harness (DSH)**: see every archived session grouped by workspace, restore one back to its exact place, or wipe its local storage for good — no ghost rows, no unopenable sessions, no reload required. **English** · [简体中文](README.md) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Platform: DSH Web](https://img.shields.io/badge/platform-DSH%20Web-4f46e5.svg)](#compatibility) [![Version](https://img.shields.io/badge/version-1.2.0-success.svg)](https://github.com/jiulanyanyue-boop/dsh-archive-manager) [![No build step](https://img.shields.io/badge/bundled%20JS-no%20build%20step-orange.svg)](#architecture) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing) **English** · [简体中文](README.md) ![Archived sessions](screenshots/e2e-01-section.png) --- ## What this is DSH's built-in "archive" is nothing more than a **hide switch**: it makes a session disappear from the sidebar. That's it. - You **cannot see** how many sessions you archived, how much space they take, or which workspace they belong to. - To find a session from three months ago, you have to scroll the entire sidebar. - To reclaim space by permanently clearing archived sessions, DSH **has no such feature**. - Worse: after deleting storage files by hand, the sidebar keeps listing **ghost sessions that fail with `ENOENT`** the moment you click them. `archive-manager` fills that gap. It adds a full page under **Settings → Archived Sessions** and turns archiving from a one-way hide into a complete lifecycle: inspect, restore, permanently delete. **Plain client JS + host routes. No build step, no npm dependencies, no external service.** Restart DSH once after installing and you're done. --- ## The real problems it solves This page is not a thin wrapper over existing APIs. Each row below is a long-standing trap that this plugin closes: | Problem | What it looks like | What this plugin does | | --- | --- | --- | | **Archived content is invisible** | Archiving means "gone from the sidebar"; you have no idea what's left | Groups everything by workspace: title / archived-at / last activity / size on disk / whether files still exist | | **No archive timestamp anywhere** | DSH only persists the *set of archived ids* — **it never stores when** | The plugin keeps its own index `archive-times.json`, records the first observation of archiving, writes atomically, and prunes on restore/delete | | **Restore can't find "where it was"** | Guess by scrolling the sidebar | Restore goes through `unarchiveSession`; the workspace record is **left untouched**, so the session returns to its exact workspace and position | | **Ghost rows survive deletion** | Files deleted, but the sidebar still lists it, it still opens, and sending a message throws `ENOENT` | Delete stops activity via `workspace/session-stop` → detaches the live session via `detachEntered` → then removes disk → clears records → drops the archive entry | | **Deletion only half happened** | Files gone but the workspace record remains, leaving a permanently unopenable entry | Delete semantics changed to "erase **every** local trace of this session": disk / records / archive set / time index | | **UI leftovers need a manual refresh** | You must close and reopen the settings page after deleting | Remove the row locally + `ctx.sessions.refresh()` + sequenced reconciliation — **effective within the same page** | | **Refresh races resurrect deleted rows** | Concurrent `/state` responses arrive out of order and stale data overwrites fresh state | `/state` reads are sequenced and single-flight; stale responses are discarded | | **Historical leftovers never get cleaned** | No file on disk, still recorded in a workspace — accumulating forever | `POST /repair` plus a silent, idempotent self-check 3 s after startup; the page also offers a one-click banner when it detects leftovers | --- ## Features | Capability | Details | | --- | --- | | **Dedicated settings page** | `Settings → Archived Sessions`, using the host-reserved `settings.section` (id `archived-sessions`, order 25, right after "Agent presets"; icon is the archive box rather than the default gear) | | **Grouped by workspace** | Each group shows workspace title + directory name + count; sessions no longer owned by any workspace go into a separate "Ungrouped sessions" group | | **Complete row metadata** | Full title (never truncated; long titles wrap), archived-at (absolute + relative), last activity, created-at, size on disk; a red hint when storage is missing | | **Restore** | Unarchives immediately and puts the session back to its original workspace and position; visible in the sidebar **without a page reload** | | **Permanent delete** | Confirmation (shows how many entries and roughly how much space) → stop activity → detach live session → remove disk → clear records → drop archive entry → clear index | | **Stale-reference cleanup** | Automatic + manual; cleanup only touches workspace records and never removes an existing session's files | | **Actions on hover** | "Restore / Delete" are hidden by default and fade in on hover or keyboard focus; always visible on touch devices — a clean list without sacrificing discoverability | | **Theme-faithful** | Colors, fonts, spacing and radii all reference host variables (`--dsw-alias-*` / `--dsw-radius-*`); looks native in light, dark, and any skin | | **Bilingual** | Dictionaries switch through the host `locale` service — not hard-coded strings | | **Accessible** | `role="dialog"` / `aria-labelledby` / Escape to close / focus handling / keyboard reachable | | **A warmer empty state** | A whale illustration instead of a cold "no data" | --- ## Screenshots **List: grouped by workspace, just the right density** ![Actions on hover](screenshots/e2e-02-hover.png) **Permanent delete: explicit confirmation with counts and size** ![Delete confirmation](screenshots/e2e-03-confirm.png) **Empty state** ![Empty state with whale](screenshots/empty-03-section.png) --- ## Installation ### Prerequisites | Item | Requirement | | --- | --- | | DSH | Web / desktop, **verified on 0.2.1-rc.2** (depends on `webServer`, `workspaceRegistry`, `sessions`) | | Node | `>= 22.19.0` (the DSH bundled runtime is enough; nothing to install) | | Build | **Not needed.** The repository already contains runnable artifacts | ### ⚠️ Read this first: do not install by bare package name This package is named `archive-manager`, but **both `archive-manager` and `dsh-archive-manager` are already taken on npm by other people**. DSH's "Add plugin" treats a bare package name as an npm package, so: | You type | What actually gets installed | Result | | --- | --- | --- | | `archive-manager` | Someone else's npm package | ❌ Different plugin, most likely incompatible with your DSH | | `dsh-archive-manager` | Someone else's npm package (peer `0.1.0-rc.6`) | ❌ `Plugin ... is incompatible with dsh 0.2.1-rc.2` | | **`https://github.com/jiulanyanyue-boop/dsh-archive-manager`** | **This repository** | ✅ Correct | **Bottom line: use the full GitHub URL, not a bare package name.** ### Option 1: GitHub URL (recommended) Open **Settings → Plugins → Add plugin** and paste: ``` https://github.com/jiulanyanyue-boop/dsh-archive-manager ``` Equivalent forms (all resolved by the DSH installer, all clone this repository): | Form | Notes | | --- | --- | | `https://github.com/jiulanyanyue-boop/dsh-archive-manager` | Simplest — paste the browser URL | | `github:jiulanyanyue-boop/dsh-archive-manager` | pnpm's GitHub shorthand | | `https://github.com/jiulanyanyue-boop/dsh-archive-manager#v1.2.0` | Pin to a tag / branch / commit (**the tag must be pushed** — see "Development & publishing") | | `git+https://github.com/jiulanyanyue-boop/dsh-archive-manager.git` | Explicit git protocol | The installer first runs a `git ls-remote` reachability check (5 s timeout by default) and then hands the clone to pnpm. > **Restart the DSH desktop app once after installing.** The profile composition is built once at startup (no HMR for profiles), so a new plugin entry only takes effect after a restart. After that, every operation (restore/delete) takes effect inside the page — no restart and no manual refresh. ### Option 2: Local directory / tarball (offline) ```powershell # Build a tarball inside the repository npm pack --pack-destination "$env:USERPROFILE\Downloads" ``` Then paste the **absolute path** of the tarball (or of the unpacked directory) into "Add plugin". The "Install source" dropdown only applies to npm package names; a local path stays offline. > Before packing, check that the `files` whitelist in `package.json` contains what you need. > GitHub installation clones the whole repository and is not limited by `files`; only `npm pack` is. ### Option 3: Manual profile mount 1. Put the repository at `/plugins/archive-manager` 2. Edit `/profiles//package.json`: - add `"archive-manager": "link:/plugins/archive-manager"` to `dependencies` - append `"archive-manager"` to `dsh.profile.bundles` 3. Restart DSH > **Install the package exactly once.** `archive-manager` is both the package name and the Loader entry id, so installing it twice creates duplicate entries. When switching sources, remove the old dependency and bundle entry first. --- ## Usage ### Archiving a session The archive action itself is DSH's own (the "Archive" item in the session list); this plugin does not change it. An archived session disappears from the sidebar **and shows up under Settings → Archived Sessions**. ### Restore Hover the target row in the list → click **Restore**. - The session is unarchived immediately and returns to its **original workspace and position**; - It shows up in the sidebar without any reload; - A notice appears at the top: *Restored "{title}"; the session list has been refreshed.* ### Permanent delete Hover → click **Delete** → confirm → **Delete permanently**. The confirmation tells you **how many storage entries and roughly how much space** will be removed. After confirming, the steps run in a fixed order: 1. `workspace/session-stop` — stop the session's turns, subagents, background jobs and reminders in parallel; 2. `detachEntered` — remove the live session from `SessionStore` and let `session/disposed` cleanup land; 3. Remove the session directory + projection cache files (if this fails, the whole operation fails and **no record is touched**); 4. `workspace.detachSession` — clear the workspace record; 5. `unarchiveSession` — drop the archive entry; 6. Clear the archive-time index. **Nothing goes to the recycle bin. It cannot be undone.** ### Cleaning stale references If the plugin finds leftovers ("recorded in a workspace, but no file on disk"), a banner appears at the top: > N stale session references found … **Clean up** Cleanup only calls `detachSession` and **never removes files of an existing session**. The plugin also runs the same idempotent self-check silently 3 seconds after startup. --- ## Compatibility | Item | Status | | --- | --- | | DSH `0.2.1-rc.2` | ✅ Verified (development and test baseline) | | DSH `0.1.0-rc.x` | ⚠️ Untested and very likely unusable (the client/host service shape differed) | | Host services missing | The plugin **does not activate** rather than rendering a fake list | | Localization | Chinese / English, following the host `locale` | > Note: this `package.json` declares **no** `peerDependencies` and no `engines.dsh` — > DSH's compatibility preflight lets plugins with no declared requirements through, and actual compatibility is decided at runtime by service probing (missing services means no activation). --- ## Architecture The plugin is a **host half + browser half** pair, structurally identical to existing DSH plugins such as `boot-splash` and `dsh-purge`. ### Files | File | Role | | --- | --- | | `host.js` | Host side: registers `/plugins/archive-manager/*` routes, reads/writes the index, performs restore/delete | | `client.js` | Browser side: `window.__ModuleLoader__.load()` form, registers the settings `settings.section` | | `cordis.patch.yml` | Bundle patch layer: declares one enabled Loader entry to the host | | `package.json` | Package manifest: `dsh.bundle.patch` points at the patch, `dsh.client.platform = web` | ### Host half (`host.js`) `inject = ['webServer', 'workspaceRegistry', 'sessions']` Registers **five `exact` routes** (deliberately not a prefix — a prefix would swallow the static materialization path of `client.js`): | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/plugins/archive-manager/ping` | Self-check (`{ ok, plugin }`) | | `GET` | `/plugins/archive-manager/state` | The whole archive tree (groups + `orphanCount`, newest archived first within each group) | | `POST` | `/plugins/archive-manager/restore` | `{ sessionId }` restore | | `POST` | `/plugins/archive-manager/delete` | `{ sessionId }` permanent delete | | `POST` | `/plugins/archive-manager/repair` | Clean stale records (idempotent) | Data sources: - Archive set and workspace records: `ctx.workspaceRegistry` (`archivedSessionIds` / `list()`) - Title, created-at, last activity: `/storages/session_projcache/sessions/.json` - Size on disk and existence: `/sessions///` - Archived-at: `/archive-manager/archive-times.json` (own index, atomic writes, 20000-session scan cap) **Concurrency control**: restore / delete / cleanup share a single Promise serial queue, so only one mutation runs at a time. ### Browser half (`client.js`) - No build step; `require('react')` comes from the host module table; **no JSX** (built directly with `React.createElement`); - Registers `settings.section`: `{ id: 'archived-sessions', order: 25, label, locale, inject }`; - Data flow: `createStore` (immutable snapshot + subscription) → `useStore` (`useState` + `useEffect`) → re-render; - Styles are injected as `