# Agent Note: 在变更前绑定 JSONL 会话身份

Status: implemented

[English](2026-07-20-jsonl-storage-identity.md) | 中文

## 问题

JSONL 查找会根据请求的会话 id 在各个项目目录中选出物理日志，而解析得到的 `SessionHeader` 会提供后续修复和追加操作使用的元数据。如果这两个事实没有绑定，为会话 A 选中的日志就能声明会话 B 的 id 或 cwd，并将修复或后续追加重定向到 B 的路径。当同一个编码后 id 出现在多个项目目录中时，项目扫描也必须给出确定的结果。SQLite 不存在这种歧义，因为主键查询会将元数据和事件绑定到请求的 id。

## 决策

`loadStored(id)` 是协调器唯一的已存前缀查找操作。JSONL 后端扫描所有项目目录，要求名称与该 id 的编码值匹配且其中包含 transcript（文本记录）的会话目录至多有一个，解析其中的 transcript，然后验证 `header.id === id`，并验证选定路径要么等于 `logPath(root, header.cwd, header.id)`，要么经文件系统路径规范化后，两种写法解析为同一份 transcript。`list()` 执行相同的路径验证，并拒绝跨项目目录重复的 id。

协调器会独立断言返回的 id，并在修复、发布状态或持久化后缀之前比较已存 cwd 和活动会话的 cwd。协调器保留一份已验证元数据的独立副本；JSONL 的追加和修复操作根据该副本派生路径。因此，`PersistenceBackend<TornMarker>` 接口既不需要限定范围的活动会话查找，也不需要存储定位器类型。

如果配置的 JSONL 根目录已存在，插件加载时该路径必须是可读目录。根目录不存在仍然是有效配置，首次物化时会创建该目录。后端对每个会话只支持一个活动写入方；在所有者完成资源释放且所有写入停止之前，另一个后端实例或进程不得变更该会话。

## 考虑过的替代方案

**按会话 id 扁平化存储。** 扁平命名空间会让重复发布在同一路径上冲突，但路径验证和重复项拒绝无需让检查依赖扁平的全局命名空间，也能消除身份缺陷。

**通过协调器传递不透明存储定位器。** 定位器可以将 JSONL 变更直接绑定到选定路径，但 JSONL 可以根据已经验证的元数据重新得到该路径。为 SQLite、测试后端、追加和修复操作增加一个泛型和参数，会让每个实现都承担只有文件后端需要的概念。

**协调多个活动写入方。** 专用协调服务、进程级全局注册表或跨进程锁会定义新的部署拓扑，而不是修复身份验证。受支持的拓扑只有一个活动写入方；禁止覆盖的硬链接发布仍会裁决初始的同 id 创建竞争。

## 后果

JSONL 日志的身份不匹配、位置错误和重复会在修复或协调器状态变更前失败。查找开销仍与项目目录数量成正比，单一活动写入方的所有权仍是明确限制。协调器和 JSONL 测试固定了修复前拒绝、两个受影响日志的字节均保持不变、列出时的路径验证、重复 id 拒绝、项目路径规范化冲突与大小写别名，以及加载时的根目录验证。
