[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)插件:**把每个项目的会话日志保存在项目目录里**,而不是全部集中在 `~/.dsh/sessions`。

DSH 默认把所有会话存在 `~/.dsh/sessions/<project-key>/<session-id>/session.jsonl.zstd`。本插件替换内置的 JSONL 持久化后端,让会话数据跟着项目走:

```
<项目目录>/.dsh/sessions/<session-id>/session.jsonl.zstd
```

克隆仓库、归档项目、换机器迁移 —— 对话历史随项目一起走。

## 它做什么

| 关注点 | 行为 |
| --- | --- |
| 存储布局 | `<cwd>/.dsh/sessions/<session-id>/session.jsonl.zstd`(文件格式与 zstd 帧完全兼容,官方读取器可直接读) |
| 内置后端 | 组合树中的 `@deepseek-ai/dsh-session-persistence-jsonl` 行会被**禁用**,由本插件提供 `ctx.sessionPersistence` |
| 旧数据 | 首次启动时,`~/.dsh/sessions` 下已有的会话**一次性**迁入各自项目目录的 `.dsh/sessions/`(按日志 header 里的 `cwd` 定位) |
| 迁不走的会话 | cwd 目录已不存在、无 cwd、或项目目录不可写的会话留在 `~/.dsh/sessions` 原地,仍可读可恢复 |
| 查找索引 | `~/.dsh/sessions/index.json` 记录 `会话 id -> 项目目录` 指针,让"按 id 恢复"与 Web 跨项目会话列表照常工作;它只含指针,不含会话数据 |
| Web 列表 | 会话列表仍按项目分组展示(合并"项目本地"与"未迁移残留"两处) |
| 无 cwd 会话 | 与官方一致,存 `~/.dsh/sessions/_no-cwd/` |

## 安装

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

然后**重启 `dsh web`**(组合树变了,必须重启)。

开发态安装(本地仓库):

```bash
# 在 ~/.dsh/profiles/web 下
# package.json dependencies 写入:
#   "@yangzhe1991/dsh-project-session-store": "link:/绝对路径/dsh-project-session-store"
pnpm install
```

## 注意事项

- **重启生效**:插件替换组合树的持久化行(把 `session-persistence-jsonl` 置为 `disabled: true`),启动时应用。
- **一次性迁移尽力而为、幂等**:失败会打日志,旧文件留在原地,下次启动重试。
- **卸载/回滚**:从 profile 移除插件(或在 `cordis.patch.yml` 里把 `session-persistence-project-local` 设为 `disabled: true` 并恢复 `session-persistence-jsonl`)。项目本地会话此时对官方后端不可见(它们本就在项目里),要么手动搬回,要么继续用本插件。
- **跨机器迁移**:把项目目录(含 `.dsh/sessions/`)搬到另一台机器后,pull 下来的会话能被列表发现,也可正常浏览/恢复 —— 插件按**文件实际所在位置**定位,不依赖 header 里的 `cwd`(它指向源机路径,本机不可解析);恢复后新事件写回同一物理文件,日志不会分裂。对外呈现的 `cwd` 会归一化为本机真实项目目录,未归组的会话会自动挂入对应(或自动创建的)web 工作区 —— 零配置、不依赖任何写死的路径映射,任何人把项目拷到任何机器,侧边栏都能自动归组。
- **`~/.dsh/sessions/index.json` 指针文件**只对"跨项目发现"不可扫描替代:删掉后,下一次 `session.list` 会靠扫描旧根+项目目录重建它,但索引外的项目本地目录只能按项目逐个发现。
- **版本耦合**:插件不自行安装官方包 —— 官方 `@deepseek-ai/*` 是 peer 依赖,运行时从宿主的模块回退目录(`$DSH_HOME/profiles/node_modules`,官方自动维护、版本随宿主配套)解析,与官方包"自装旧副本 + 新官方链"的混装天然隔离(避免 dsh-session 与 dsh-llm 版本错配导致无法启动)。宿主升级后无需同步改插件;官方包 API 大改时插件需发新版适配。

## 配置

插件行支持以下可选项(均可省略):

```yaml
- id: session-persistence-project-local
  name: '@yangzhe1991/dsh-project-session-store'
  config:
    # 集中根:旧数据回退位置 + 指针索引所在目录。默认 $DSH_HOME/sessions
    # root: /你的/自定义/根
    # 项目内的会话目录名。默认 .dsh/sessions
    # projectDirName: .dsh/sessions
```

## 工作原理

- 继承官方 `JsonlSessionPersistence`,复用其 coordinator、zstd 编码、崩溃修复(torn-tail)与耐久性保证 —— 只覆写路径解析、列举与发现逻辑。
- 写路径:`<cwd>/.dsh/sessions/<id>/session.jsonl.zstd`(无 cwd 的会话回落到集中根 `_no-cwd/`)。
- 读路径(按 id 查找):先查指针索引,再回退扫描旧集中根;命中后把物理位置记在内存里,后续追加永远不分裂日志。
- 一次性迁移:把每条旧日志搬进项目目录(同设备 `rename`,跨设备 `EXDEV` 回退 copy+unlink),更新索引,清理空壳旧目录。

## License

MIT
