# Agent Note: 按项目分组的会话目录

Status: implemented

[English](2026-07-24-project-session-directories.md) | 中文

## 问题

持久化根目录可以只供一个项目使用，也可以由多个项目共享，还可以是临时目录或集中式目录。对 cwd 进行哈希得到的分桶目录能适用于所有这些部署方式，但开发者无法从目录名辨认项目，因此共享根目录难以浏览。

每个 JSONL 会话也直接以单个文件的形式放在项目分桶目录中。这种布局没有为元数据、附件、溢写文件或协调状态等其他会话产物提供归属目录。

## 决策

JSONL 后端按可读的项目键存储会话，并为每个会话提供独立目录：

```text
<configured-root>/
  --<normalized-cwd>--/
    <encoded-session-id>/
      session.jsonl.zstd
```

原始模式使用 `session.jsonl`，没有 cwd 的会话使用 `_no-cwd`。文件系统路径分隔符和驱动器分隔符会转换为 `-`，不安全的代码单元使用 `~XXXX`，可读名称则限制长度，以确保目录项不超过文件系统限制。

项目键有意不带哈希后缀。这遵循 coding agent（编码智能体）常用的易读约定，使规范化后的项目路径本身就是完整的目录名。规范化过程有损：`/a/b-c` 与 `/a-b/c` 等路径，或者保留前缀相同的长路径，会共用同一个项目目录。不同的会话 id 仍会选择不同的会话目录；复用相同的会话 id 仍构成存储冲突，系统会予以拒绝。

在不区分大小写的文件系统上，大小写不同的项目键也可能指向同一个物理目录。只有当文件系统路径规范化将发现路径和预期路径解析为同一个 transcript（文本记录）时，身份验证才接受这种拼写变体。规范化后的路径如果不同，仍视为存储损坏，因此大小写别名不会让区分大小写的存储放宽同一 id 的冲突检查。

根目录由部署配置决定。这种布局既不选择全局根目录，也不要求项目共享根目录。部署选择集中存储时，目录名仍能让项目路径易于辨认；使用项目本地根目录时，也采用同样的确定性结构。

编码后的会话 id 用于命名归属目录，而不是 transcript 文件本身。`SessionPersistence.locate()` 仍返回固定的 transcript 路径，从而保持钩子 `transcript_path` 和 `DSH_SESSION_JSONL` 的语义不变。发现过程会忽略会话目录中的其他条目，因此后端以后添加会话自有产物时无需再次改变布局。

延迟物化仍以 transcript 为界：`create()` 不执行文件系统 I/O，首次追加会先创建项目目录和会话目录，再以无冲突方式发布 transcript。空目录不会被列为会话。后端会显式报告布局错误并拒绝扁平的 `<project>/<id>.jsonl*` 产物；预发布格式不提供自动数据迁移。

## 考虑过的替代方案

**保留不透明的 cwd 哈希。** 这可以保持目录名简短，但当多个项目共享一个持久化根目录时，无法满足按项目路径浏览的需求。

**把会话文件直接放入各项目目录。** 这与 Claude Code 和 pi 的基本文件组织一致，但没有为未来产物提供会话级归属边界。

**添加防冲突的哈希后缀。** 这种方式能区分规范化形式相同的路径，但会使目录名不再只是规范化后的项目路径。所选约定接受有损的项目分组，以换取更简单、易于辨认的名称。

**强制使用集中式根目录。** 不予采纳，因为存储位置属于部署配置。项目分组在根目录共享时有用，在不共享时也没有负面影响。

**同时加载扁平布局和目录布局。** 按照预发布阶段不提供兼容性的原则，不予采纳。只接受一种布局，可以让身份检查和发现过程保持确定性。

## 后果

共享存储可以通过易于辨认的项目名进行浏览，本地根目录和自定义根目录则继续保有现有的配置自由。每个会话都有一个可供后端未来存放自有产物的目录，而现有 transcript 消费方仍会收到文件路径。

项目目录名比原先由 12 个十六进制字符组成的 cwd 哈希更长。路径很长时，目录名只显示长度受限的前缀。移动项目通常会选择不同的目录，但按设计，不同的 cwd 字符串如果规范化成相同名称，就会共用同一个项目目录。
