# dsh-archive-manager
[](README.md)
[](README_en.md)
A DeepSeek Harness (DSH) web-GUI plugin: adds an "Archive" page to the settings window for viewing, filtering, sorting, unarchiving and permanently deleting archived DSH sessions.
[](LICENSE)
[](package.json)
[](https://github.com/deepseek-ai/deepseek-harness)
---
## 📑 Table of Contents
- [📸 Preview](#-preview)
- [✨ Features](#-features)
- [🚀 Quick Start](#-quick-start)
- [📖 Usage](#-usage)
- [🔧 How It Works](#-how-it-works)
- [⚠️ Technical Notes & Limitations](#️-technical-notes--limitations)
- [⚙️ Configuration](#️-configuration)
- [🤝 Contributing](#-contributing)
- [📄 License](#-license)
---
## 📸 Preview

*Workspace-grouped list + filter/sort toolbar*
---
## ✨ Features
| Feature | Description |
|---------|-------------|
| **View archived sessions** | A new "Archive" page in the settings window lists all archived sessions, grouped by workspace; sessions not belonging to any workspace go under "(Unassigned)" |
| **Filter** | Filter archived sessions by "All workspaces", a specific workspace, or "Ungrouped"; the dropdown only lists workspaces that **own at least one archived session**, and the "Ungrouped" option only appears when ungrouped archived sessions exist |
| **Sort** | Sort by session name (alphabetical asc/desc) or by session creation time (asc/desc) |
| **Unarchive** | Removes a session from the archive set; it **reappears in the corresponding workspace group in the left sidebar** and can be opened |
| **Delete (with confirmation)** | Delete a single session, or "Delete all" at once; every delete is guarded by a second confirmation dialog — it only runs after clicking "Confirm delete" |
| **Permanent deletion** | Deletion removes the session log file (`session.jsonl.zstd`), the archive marker and the workspace bookkeeping — irreversible |
| **Bilingual UI** | UI text follows DSH's active language (中文 / English) |
---
## 🚀 Quick Start
### Prerequisites
- DSH CLI and pnpm installed (`dsh plugin` forwards to pnpm internally)
### Install
```sh
# Option 1: install from a local source directory (development)
dsh plugin --profile web add dsh-archive-manager@link:
# Option 2: install from GitHub
dsh plugin --profile web add github:Ycet/dsh-archive-manager
```
The package declares a `dsh.bundle` patch layer; `dsh plugin` merges the loader entry into the profile's bundle layer automatically — no manual editing of `cordis.patch.yml` required.
### Launch
1. Restart the web app: `dsh web`
2. Open http://127.0.0.1:3080 and refresh the page
3. Click **Settings** at the bottom of the left sidebar, then choose the **Archive** page
> [!NOTE]
> When installed as a `file:` snapshot (copy), re-run the install command after changing the source to refresh the snapshot inside the profile, then restart DSH web for it to take effect (bundle-layer changes always require a DSH restart; they are not hot-reloaded).
---
## 📖 Usage
1. Click **Settings** at the bottom of the left sidebar;
2. Select the **Archive** page on the left of the settings window;
3. Toolbar at the top of the page:
- **Filter** dropdown: default "All workspaces"; pick a specific workspace or "Ungrouped" to narrow down; the dropdown only lists workspaces that **own at least one archived session** (a workspace with no archived sessions does not appear), and the "Ungrouped" option only appears when ungrouped archived sessions exist;
- **Sort** dropdown: by name or creation time, ascending or descending;
- **Delete all** button: deletes every archived session under the current filter (with confirmation);
4. Sessions are grouped by workspace, each group showing session titles and creation times:
- **Unarchive**: the session returns to its workspace group in the left sidebar and can be reopened;
- **Delete**: permanently deletes the session after a second confirmation.
---
## 🔧 How It Works
### Underlying storage of archiving in DSH
DSH persists session archive state in `~/.dsh/storages/workspace.json` (the workspace domain):
```jsonc
{
"global": {
"initialized": true,
"workspaceIds": ["..."],
"archivedSessionIds": ["session-xxx", "..."],
},
"tables": {
"workspaces": {
"": {
"path": "/abs/path",
"title": "Workspace name",
"sessionIds": ["..."],
"createdAt": "...",
"updatedAt": "..."
}
}
}
}
```
`archivedSessionIds` is the global archive set. Archiving only hides a session from the partition views; it **does not** delete logs or change workspace bookkeeping, so unarchiving restores the session to its original workspace.
### Data flow
```mermaid
flowchart LR
subgraph Browser
A["Settings → Archive page settings.section"] -->|"fetch same-origin JSON"| R
A -->|"React UI: grouping/filter/sort/confirm dialogs"| A
R["Host webServer routes"] --> A
end
subgraph Host
R -->|"/api/archive-manager/list"| H1["Read workspace domain + session header/title"]
R -->|"/api/archive-manager/unarchive"| H2["Remove from archivedSessionIds"]
R -->|"/api/archive-manager/delete"| H3["Remove archive marker + bookkeeping, delete log dir"]
R -->|"/api/archive-manager/delete-all"| H4["Delete all archived sessions one by one"]
end
H1 --> WS["storageDomain.get('workspace')"]
H1 --> SP["sessionPersistence / sessionQuery"]
H2 --> WS
H3 --> WS
H3 --> FL["await sessionPersistence.flush durability barrier"]
FL --> FS["fs.rm delete session log dir"]
H4 --> H3
```
After the host writes the `workspace` domain global, DSH validates the domain state on startup (fail-loud). Writes keep the `workspace.json` schema structure strictly unchanged (arrays are replaced wholesale before writing back), so DSH startup is never broken.
---
## ⚠️ Technical Notes & Limitations
- **No official unarchive / delete-session API**: the DSH host `workspaceRegistry` only exposes `archiveSession` — there is no `unarchiveSession` and no "delete session" API. This plugin reads/writes the `storageDomain.get('workspace')` domain store (`~/.dsh/storages/workspace.json`) directly, strictly preserving its schema.
- **Deletion is permanent**: deleting a session removes the log files under `~/.dsh/sessions///` — irreversible. DSH's SQLite search index cleans up the session on the next reconciliation.
- **Files are deleted before records are cleared**: deletion first locates the session log directory and only after the log file deletion **succeeds** does it remove the archive marker and workspace bookkeeping; if log deletion fails (cannot locate, still exists, IO error), it returns an error and **leaves the archive state untouched** — the session stays hidden and does not "resurrect" in the sidebar.
- **Persistence snapshot compatibility and deletion verification**: DSH 0.1.5-rc.1 returns `{ header, revision }` from `sessionPersistence.list()`, not bare headers. Reading `id` / `cwd` directly missed the real session; when fallback paths also failed, deletion incorrectly cleared the archive marker while leaving the log on disk. Snapshots are now unpacked (with legacy bare-header support), and the actual header is passed to `locate()`, supporting custom storage roots and legitimate `_no-cwd` paths. Available `stat(id)` is used to verify deletion. Read errors, incompatible records, remaining sessions, and unresolvable listed sessions preserve archive markers and report failure.
- **Live persistence drain safeguard**: the plugin retains `await sessionPersistence.flush()` after detaching an idle live session, and refuses to clear archive markers when that capability is missing or fails. This drains currently active write handles. The previous asynchronous-rewrite root-cause claim was not reproduced against the real backend; the defect reproduced and fixed in this investigation is incorrect snapshot parsing.
- **Takes effect live**: writing the global triggers `domain/changed`; DSH pushes `host/archived-sessions-changed` to the browser, so the sidebar and the archive page refresh immediately.
- **In-memory cache consistency (fixed)**: DSH's `WorkspaceRegistry` is the single writer and its `archivedSessionIds` getter reads the in-memory `state` directly. When rewriting the store, this plugin **synchronously updates `registry.state.archivedSessionIds`** and uses the official `WorkspaceEntity.detachSession` to remove workspace bookkeeping (also refreshing the entity.record cache); so unarchive → archive cycles and hard-refresh baseline rebuilds never lose or hide sessions.
- **Archived sessions only**: delete/unarchive first validates that the session actually exists in `archivedSessionIds`; non-archived sessions are never touched.
- **Live-session handling (fixed)**: after an agent finishes, its session still stays in DSH memory as a live Session (`ctx.sessions`); deleting files alone does not remove it from the frontend (`session.list` still returns the live part and the sidebar shows it "resurrected"). When deleting a live session: deletion is refused **only if the agent is truly running (`agent.status === "running"`)**; for idle live sessions (agent idle or no agent) it first calls `sessions.detachEntered` to remove it from memory (triggers `session/disposed` → DSH pushes `api-session/removed` → the sidebar removes it immediately, and the agent-loop cleans up the associated idle agent), then **awaits the durability barrier** and only afterwards deletes the files and clears the records.
- Caveat: when the frontend receives `api-session/removed` for a row whose `origin === "subagent"` (or whose id is address-retained), DSH downgrades row removal to a `running: false` flip and keeps the row; for such sessions judge by the result after a hard refresh.
> [!WARNING]
> "Delete" and "Delete all" are **permanent deletions**: the session log file, archive marker and workspace bookkeeping are all removed — **irreversible**.
---
## ⚙️ Configuration
This plugin needs no environment variables or config files — it works out of the box. It adds no public DSH RPC and mounts no new Service; it only registers 4 package-local same-origin HTTP routes:
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/archive-manager/list` | Return archived sessions + workspace list |
| `POST` | `/api/archive-manager/unarchive` | Unarchive a single session |
| `POST` | `/api/archive-manager/delete` | Permanently delete a single archived session |
| `POST` | `/api/archive-manager/delete-all` | Permanently delete all archived sessions |
---
## 🤝 Contributing
Issues and pull requests are welcome: report problems with the DSH version, plugin version, reproduction steps and logs at [Issues](https://github.com/Ycet/dsh-archive-manager/issues); for improvements, follow Fork → branch → PR (host changes go to `index.js`, browser changes to `client.js`).
---
## 📄 License
This project is licensed under the [MIT](LICENSE) license.