# 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 字符串如果规范化成相同名称，就会共用同一个项目目录。
