DSH / Atlas
2026-07-20implementedbug-fixarchived 2026-09-30

Bind JSONL session identity before mutation

在变更前绑定 JSONL 会话身份

JSONL lookup selects a physical log from the requested session id across project directories, while the parsed `SessionHeader` supplies the metadata used by later repair and append operations. Without binding those two facts, a log selected for session A can declare session B's id or cwd and redirect a repair or later append to B's path. The project scan also needs a defined result when the same encoded id exists in

English

Problem

JSONL lookup selects a physical log from the requested session id across project directories, while the parsed SessionHeader supplies the metadata used by later repair and append operations. Without binding those two facts, a log selected for session A can declare session B's id or cwd and redirect a repair or later append to B's path. The project scan also needs a defined result when the same encoded id exists in more than one project directory. A medium that resolves records through one authoritative key may avoid this ambiguity, but the shipped JSONL provider must bind its selected path explicitly.

Decision

findLog(id) is the JSONL provider's single physical resolver. It scans every project directory and requires at most one matching encoded session directory with a transcript. For a supported historical snapshot, the generation operation translates its header in memory, then validates header.id === id and that the selected path either equals logPath(root, header.cwd, header.id) or filesystem canonicalization resolves both spellings to the same transcript before any write. The fused loadCurrentStored(id) decodes the resulting current prefix without another file read; an already-current snapshot takes the no-write fast path and applies the same identity check during that decode. The ordinary current loadStored(id) hook retains the identity validation for coordinator fallbacks. list() applies the same path validation and rejects duplicate ids across project directories.

The coordinator independently asserts the returned id and compares the stored cwd with a live session's cwd before repair, state publication, or suffix persistence. It keeps a detached copy of validated metadata; JSONL append and repair derive their path from that copy. The PersistenceBackend<TornMarker> interface therefore needs neither a scope-specific live lookup nor a storage-locator type.

An existing configured JSONL root must be a readable directory when the plugin loads. An absent root remains valid and is created on first materialization. The backend supports one live writer per session; another backend instance or process must not mutate that session until the owner finishes disposal and all writes stop.

Alternatives considered

Flatten storage by session id. A flat namespace makes duplicate publication collide on one path, but path validation and duplicate rejection close the identity defect without making the check depend on a flat global namespace.

Carry an opaque storage locator through the coordinator. A locator binds JSONL mutations directly to a selected path, but JSONL can reproduce that path from metadata it has already validated. Adding another generic and argument to the coordinator, test backends, append, and repair makes every implementation carry a concept only the file backend needs; an out-of-tree provider keeps its medium-specific locator inside its own primitives.

Coordinate multiple live writers. A dedicated coordination service, process-global registry, or cross-process lock would define a new deployment topology rather than repair identity validation. The supported topology has one live writer; no-overwrite hard-link publication still arbitrates an initial same-id creation race.

Consequences

Mismatched, misplaced, and duplicate JSONL logs fail before repair or coordinator state mutation. Lookup remains proportional to the number of project directories, and one-live-writer ownership remains an explicit limitation. Coordinator and JSONL tests pin rejection before repair, unchanged bytes for both affected logs, path validation during listing, duplicate-id rejection, normalized-project collisions and case aliases, and load-time root validation.

中文

问题

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

决策

findLog(id) 是 JSONL provider 唯一的物理解析器。它扫描所有项目目录,并要求名称与该 id 的编码值匹配且其中包含 transcript(文本记录)的会话目录至多有一个。对于受支持的历史快照,generation 操作会在内存中转换其 header,然后验证 header.id === id,并验证选定路径要么等于 logPath(root, header.cwd, header.id),要么经文件系统路径规范化后,两种写法解析为同一份 transcript;任何写入都必须在该检查之后。融合的 loadCurrentStored(id) 会在不再次读取文件的情况下解码所得当前前缀;已经是当前格式的快照采用无写入快速路径,并在该次解码中执行相同的身份检查。普通当前格式 loadStored(id) 钩子为协调器 fallback 保留身份校验。list() 执行相同的路径校验,并拒绝跨项目目录重复的 id。

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

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

考虑过的替代方案

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

通过协调器传递不透明存储定位器。 定位器可以将 JSONL 变更直接绑定到选定路径,但 JSONL 可以根据已经验证的元数据重新得到该路径。为协调器、测试后端、追加和修复操作增加一个泛型和参数,会让每个实现都承担只有文件后端需要的概念;仓库外 provider 把自己的介质定位器保留在自身原语内。

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

后果

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