[English](README.md) | [中文](README.zh.md)

# dsh-project-session-store

[![npm version](https://img.shields.io/npm/v/@yangzhe1991/dsh-project-session-store?color=green)](https://www.npmjs.com/package/@yangzhe1991/dsh-project-session-store)
[![npm downloads](https://img.shields.io/npm/dt/@yangzhe1991/dsh-project-session-store)](https://www.npmjs.com/package/@yangzhe1991/dsh-project-session-store)
[![license](https://img.shields.io/npm/l/@yangzhe1991/dsh-project-session-store)](https://github.com/yangzhe1991/dsh-project-session-store/blob/main/LICENSE)
[![dsh-plugin](https://img.shields.io/badge/dsh-plugin-blue)](https://github.com/deepseek-ai/deepseek-harness)

DSH (DeepSeek Harness) plugin: keep **each project's session logs inside the project directory**, instead of everything centralized under `~/.dsh/sessions`.

By default DSH stores every session under `~/.dsh/sessions/<project-key>/<session-id>/session.jsonl.zstd`. This plugin replaces the built-in JSONL persistence backend so session data lives with the project:

```
<project>/.dsh/sessions/<session-id>/session.jsonl.zstd
```

Clone a repo, archive it, or move it to another machine — the conversation history travels with the project.

## What it does

| Concern | Behavior |
| --- | --- |
| Storage layout | `<cwd>/.dsh/sessions/<session-id>/session.jsonl.zstd` (same file format, same zstd frames — fully compatible readers) |
| Built-in backend | `@deepseek-ai/dsh-session-persistence-jsonl` is **disabled** in the composition tree; this plugin provides `ctx.sessionPersistence` instead |
| Legacy data | On first startup, sessions already under `~/.dsh/sessions` are migrated **once** into their project's `.dsh/sessions/` (keyed by each log's header `cwd`) |
| Unmovable sessions | Sessions whose `cwd` is gone, sessions without a `cwd`, and log files whose project directory is not writable stay in place under `~/.dsh/sessions` and remain readable/resumable |
| Lookup index | A small pointer file `~/.dsh/sessions/index.json` maps `session-id -> project dir` so resume-by-id and the cross-project Web session list keep working; it contains pointers only, never session data |
| Web UI | Session list still shows all projects grouped by project directory (it merges project-local plus any unmigrated leftovers) |
| No `cwd` sessions | Stay under `~/.dsh/sessions/_no-cwd/`, exactly like the stock backend |

## Installation

```bash
dsh plugin --profile web add @yangzhe1991/dsh-project-session-store
```

Then restart `dsh web` (restarting is required — the composition tree changes).

Dev-mode install (local checkout):

```bash
# in ~/.dsh/profiles/web
# package.json dependencies:
#   "@yangzhe1991/dsh-project-session-store": "link:/absolute/path/to/dsh-project-session-store"
pnpm install
```

## Notes & caveats

- **Restart to activate.** The plugin swaps the persistence row of the composition tree (`session-persistence-jsonl` gets `disabled: true`), applied at boot.
- **One-time migration is best-effort and idempotent.** Failures are logged and the leftover stays in the old location. A later startup retries it.
- **Uninstall / rollback**: remove the plugin from the profile (or set `session-persistence-project-local: disabled: true` and re-enable `session-persistence-jsonl` in `cordis.patch.yml`). Project-local sessions are then not visible to the stock backend — they live in the project. Either migrate them back manually or keep using this plugin.
- **Cross-machine copy**: when the project directory (including `.dsh/sessions/`) is moved to another machine, copied sessions are discovered (listed) and remain fully readable/resumable — the plugin resolves them by where the files actually live, not by the header `cwd` (which points at the source machine's path, unresolvable here). After a resume, new events are written back into that same physical file, so the log never splits. The exposed `cwd` is normalized to the session's real project directory on this machine, and ungrouped sessions are automatically attached to the matching (or auto-created) web workspace via the stock WorkspaceRegistry API — zero configuration, no hard-coded path mappings; anyone copying a project to any machine gets sidebar grouping out of the box.
- **The `~/.dsh/sessions/index.json` pointer file is irreplaceable-by-scan** only for cross-project discovery: if it is deleted, the next `session.list` rebuilds it by scanning the old root, but project-local dirs outside the index are then only discoverable per-project.
- **Version coupling**: the plugin does not install official packages itself — the `@deepseek-ai/*` packages are peer dependencies resolved from the host's module fallback directory (`$DSH_HOME/profiles/node_modules`, maintained by the harness, versioned with the host). This isolates it from mixed-install version mismatch (e.g. an old self-installed `dsh-session` meeting a newer `dsh-llm` breaking startup). No plugin change needed after a host upgrade unless official APIs change.

## Configuration

The plugin row accepts these options (all optional):

```yaml
- id: session-persistence-project-local
  name: '@yangzhe1991/dsh-project-session-store'
  config:
    # Central root: legacy fallback + pointer index location. Default: $DSH_HOME/sessions
    # root: /your/custom/root
    # Directory inside each project. Default: .dsh/sessions
    # projectDirName: .dsh/sessions
```

## How it works

- Subclasses the official `JsonlSessionPersistence`, reusing its coordinator, zstd encoding, crash repair (torn-tail), and durability guarantees — only path resolution, listing, and discovery are overridden.
- Write path: `<cwd>/.dsh/sessions/<id>/session.jsonl.zstd` (sessions without `cwd` fall back to the central `_no-cwd/`).
- Read path (`findLog` by id): pointer index first, then a legacy-root scan fallback; the physical location hit is remembered in-memory so follow-up appends never split the log.
- One-time migration: moves each legacy log into its project dir (same filesystem `rename`, `EXDEV` fallback copy+unlink), updates the index, and prunes emptied legacy directories.

## License

MIT
