DSH / Atlas
2026-07-24implementedarchitecture

Project-grouped session directories

按项目分组的会话目录

A persistence root may be local to one project, shared by several projects, temporary, or centralized. The hashed cwd buckets kept all deployments functional but made a shared root difficult to navigate because a developer could not recognize a project from its directory name. Each JSONL session also occupied one file directly inside the project bucket. That shape had no ownership directory for additional session art

English

Problem

A persistence root may be local to one project, shared by several projects, temporary, or centralized. The hashed cwd buckets kept all deployments functional but made a shared root difficult to navigate because a developer could not recognize a project from its directory name.

Each JSONL session also occupied one file directly inside the project bucket. That shape had no ownership directory for additional session artifacts such as metadata, attachments, spill files, or coordination state.

Decision

The JSONL backend stores sessions under a readable project key and gives every session its own directory:

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

Raw mode uses session.jsonl, and sessions without a cwd use _no-cwd. Filesystem and drive separators become -, unsafe code units use ~XXXX, and the readable name is bounded to keep the component within filesystem limits.

The project key intentionally has no hash suffix. This follows the common human-readable convention used by coding agents and keeps the normalized project path as the complete directory name. The normalization is lossy: paths such as /a/b-c and /a-b/c, or long paths with the same retained prefix, share one project directory. Their distinct session ids still select separate session directories; reuse of the same session id remains a storage collision and is rejected.

Case-insensitive filesystems can also make differently cased project keys refer to one physical directory. Identity validation accepts such an alternate spelling only when filesystem canonicalization resolves the discovered and expected paths to the same transcript. A different canonical path remains corruption, so case aliases do not weaken the same-id collision check on case-sensitive stores.

The configured root remains a deployment choice. The layout neither selects a global root nor requires projects to share one. When a deployment does centralize storage, project paths remain recognizable; a project-local root uses the same deterministic structure.

The encoded session id names an ownership directory rather than the transcript itself. SessionPersistence.locate() continues to return the fixed transcript path, preserving hook transcript_path and DSH_SESSION_JSONL semantics. Discovery ignores other entries inside the session directory so the backend can add session-owned artifacts without another layout change.

Lazy materialization remains tied to the transcript: create() performs no filesystem I/O, and the first append creates the project/session directories before collision-safe transcript publication. Empty directories are not listed as sessions. The backend rejects flat <project>/<id>.jsonl* artifacts with an explicit layout error; the pre-release format provides no automatic data migration.

Alternatives considered

Keep opaque cwd hashes. This preserved short names but defeated the requested navigation by project path when several projects share a persistence root.

Put session files directly in each project directory. This matched Claude Code and pi's basic file organization but left no session-level ownership boundary for future artifacts.

Add a collision-resistant hash suffix. This distinguishes paths whose normalized forms collide, but makes the directory name more than the normalized project path. The chosen convention accepts lossy project grouping in exchange for the simpler, recognizable name.

Mandate a centralized root. Rejected because storage placement belongs to deployment configuration. Project grouping is useful when roots are shared and harmless when they are not.

Load both flat and directory layouts. Rejected under the pre-release no-compatibility stance. One accepted layout keeps identity checks and discovery deterministic.

Consequences

Shared stores can be navigated by recognizable project names, while local and custom roots keep their existing configuration freedom. Every session has a directory available for future backend-owned artifacts, and existing transcript consumers still receive a file path.

Project directory names are longer than the former 12-hex cwd hashes. Very long paths show only a bounded prefix. Moving a project usually selects a different directory, but distinct cwd strings that normalize to the same name share one project directory by design.

中文

问题

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

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

决策

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

<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_pathDSH_SESSION_JSONL 的语义不变。发现过程会忽略会话目录中的其他条目,因此后端以后添加会话自有产物时无需再次改变布局。

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

考虑过的替代方案

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

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

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

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

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

后果

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

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