DSH / Atlas
2026-08-10implementedarchitecture

Session log versioning — one integer, an upgrade chain, and a per-event ignorable marker

Session log 版本机制:单调整数、升级器链、逐事件可忽略标记

Session logs must be upgradable after release, and the runtime that ships first is the floor for every later decision: whatever refusal and degradation behavior is missing from the first released reader can never be added to the copies users already run. Release issue #1901 required at minimum that an old runtime reading a newer session format reports "unsupported" instead of misreading it. The pre-change reader did

English

Problem

Session logs must be upgradable after release, and the runtime that ships first is the floor for every later decision: whatever refusal and degradation behavior is missing from the first released reader can never be added to the copies users already run. Release issue #1901 required at minimum that an old runtime reading a newer session format reports "unsupported" instead of misreading it. The pre-change reader did the opposite on both axes: assertVersion rejected any version mismatch with one direction-blind message, and the JSONL decoder passed unknown event types through untouched, so reconstruction silently skipped them — resuming a gutted session with no diagnostic at all.

Decision

The reader-compatibility review decision supersedes the structural bump criterion. This note owns the integer, unknown-event marker, direction-aware refusal, and generation-preservation decisions.

One monotonic integer, no major/minor split. Whether a version step is auto-upgradable is a property of that step — expressed by whether its upgrader exists — not something a two-level numbering scheme should promise in advance; design time rarely reveals whether the next change will turn out "major".

Bump when existing discriminators cannot prevent unsafe interpretation. Successful parsing alone does not establish compatibility: silently skipping content required for reconstruction is a wrong read. Same-version changes require safe old-reader handling and preservation of historical meaning by new readers. The linked reader-compatibility decision defines the review obligations.

Read rules by direction. Equal version: read normally. Newer than the reader: refuse, name the direction ("written by a newer harness — upgrade"), and point at the raw log artifact so the user can still see the text (SessionFormatUnsupportedError, distinct from SessionPersistenceCorruptionError because nothing is damaged). Older than the reader: every event-body operation first runs the complete adjacent chain in memory and leaves the source path, bytes, and inode unchanged. Read handles may consume that current logical result directly; a write open exclusively publishes the final current generation under its canonical versioned filename before append. Header-only listing remains non-mutating and reports the numerically highest canonical generation. Catalog generation and module initialization reject a missing adjacent step, so a published first-party build never exposes a partial historical chain. Retained lower generations are not automatic fallback or a downgrade compatibility promise.

A per-event ignorable marker covers vocabulary growth, so ordinary event additions never bump the version. The event vocabulary is decided by which plugins are mounted, which a single version integer cannot describe. A reader meeting an unrecognized event type refuses to interpret the log unless the event carries ignorable: true in its envelope. The default is required: forgetting the marker over-refuses a resumable session (an inconvenience), while a default of ignorable would make the same mistake silently resume a gutted one (a safety failure). The architecture makes this sound: model-visible content flows only through the four surfaceOp-marked surface event types plus the request/header/request/context folds, so the dangerous unknowns are exactly the non-surface events that change how the rest of the log is read (session/end-seed is the existing example).

Accepted machine records remain immutable. The accepted compatibility checkpoint preserves accepted declarations and schemas. Later changes receive new acknowledgements, and a version-bump decision needs its own increasing header transition. The checkpoint does not change the unknown-event protocol.

Writer and publication authority

SESSION_FORMAT_VERSION owns the checkout writer number; the release-status reference owns one bilingual latestReleasedVersion and evidenceTag record. Publication changes independently of source development, so status is derived by comparing those facts rather than maintaining a second released boolean. General documentation links to these authorities; fixed-version contracts and historical evidence keep their explicit numbers.

The documentation-standard check validates record structure, bilingual equality, evidence-link consistency, and the local release/writer ordering without network access. It proves internal consistency, not publication or freshness. The release operator verifies publication and updates the record after a higher format ships, as required by the release process. This keeps compatibility review independent of credentials and GitHub availability while making the manual freshness obligation explicit.

Consequences

What shipped in v0 (release 0812): direction-aware refusal with the raw-log path; the unknown-event guard against a generated known-vocabulary list (KNOWN_SESSION_EVENT_TYPES, emitted by gen-persistence-catalog from every SessionEventMap merge and kept fresh by verify-persistence-catalog); the ignorable envelope field accepted by seed validation, JSONL, and the BFF wire schema. V1 adds the static adjacent catalog, the identity v0-to-v1 edge, header-only descriptors, exact-generation JSONL publication, and current-only restoration described in Released Session formats. Historical Session read preparation owns the JSONL timing between in-memory restoration and write publication. V2 keeps the physical codec neutral to ordinary event vocabulary and payload additions: the adjacent edge freezes its released source and target inventories, while equal-version restoration applies the installed known-event set and current payload semantics. First-party writers do not set ignorable through Session.append, while a repository-external plugin is a current consumer; equal-version retention lives in the external-plugin retention decision, and the stricter historical rule lives in the alpha migration refusal decision. The unknown-type guard remains read-side because append-time vocabulary refusal would stall a live session's durability. JSONL classifies foreign versions from the minimal raw header before current-header or event parsing, so a structurally different future format reports the upgrade direction instead of "corrupt".

Alternatives considered

  • Major/minor versioning — the "is it convertible" bit lives on each step's upgrader, and pre-committing it into a number shape invites wrong promises.
  • Default-ignorable unknown events — inverts the failure mode of a forgotten marker from visible over-refusal into silent corruption.
  • Migrating during header-only listing — makes cheap inventory mutate storage and requires event bodies to compute facts that a header cannot prove. Listing returns descriptors; event-body reads own publication.
  • Per-plugin runtime registration of known event types — rejected because it would make the known set composition-dependent and register event names without classifying whether omission is safe. The persisted ignorable marker keeps that classification with each record; the external-plugin retention decision owns the current consumer constraint.
  • Duplicate release flags or runtime status services — introduce another mutable authority for a maintainer fact that does not control Session execution. The writer constant and publication record suffice.
  • Network-dependent documentation gates or publication automation — network queries would couple local documentation checks to credentials and GitHub availability; a runtime service or publication workflow change is unnecessary for record consistency. Publication verification remains an explicit release-operator obligation.

中文

问题

Session log 在发布后必须能升级格式,而最先发布的运行时决定了此后一切的下限:第一个发布版的读取器缺少哪种拒绝和降级行为,用户手里已经装上的副本就永远补不上。发布 issue #1901 的最低要求是老运行时读到新 Session 格式时明确报不支持,而不是读错。改动前的读取器在两个方向上都做反了:assertVersion 对任何版本不匹配抛出同一条不区分方向的消息;JSONL 解码器把不认识的事件类型原样放行,重建时静默跳过,恢复出一个内容残缺的会话且没有任何诊断。

决定

读取器兼容性评审决策取代结构性升版本标准。本说明负责整数版本、未知事件标记、区分方向的拒绝与代际保留决策。

**一个单调递增的整数,不分大小版本。**某一步能不能自动升级是那一步自己的属性,由它的升级器存在与否表达,不该由两级编号方案提前承诺;设计时很少能预知下一个变更算不算"大"。

既有判别信息无法阻止不安全解释时升版本。 仅解析成功不能证明兼容:静默跳过重建所需内容属于错误读取。同版本变更要求旧读取器安全处理,并由新读取器保留历史含义。所链接的读取器兼容性决策定义评审义务。

**读取规则按方向区分。**版本相等:正常读。比读取器新:拒绝,说明方向("由更新的 harness 写入,请升级"),并给出原始日志文件的路径,用户仍能看到文本(SessionFormatUnsupportedError,与 SessionPersistenceCorruptionError 区分,因为数据没有损坏)。比读取器旧:每个事件正文操作先在内存中运行完整相邻链,并保持源路径、字节与 inode 不变。读句柄可以直接使用该 current 逻辑结果;写 open 则在 append 前把最终 current generation 排他发布到其规范版本文件名。仅 header 的列表保持不变更,并报告数值最高的规范 generation。catalog 生成与模块初始化会拒绝缺失的相邻步骤,因此已发布第一方 build 绝不会暴露不完整历史链。保留的低 generation 不是自动 fallback,也不构成 downgrade compatibility 承诺。

**逐事件的 ignorable 标记吸收词汇表增长,普通的新增事件永远不用升版本。**事件词汇表由挂载了哪些插件决定,单个版本整数描述不了它。读取器遇到不认识的事件类型时拒绝解读日志,除非该事件的信封带 ignorable: true。默认为必需:忘写标记的后果是把一个本可恢复的会话拒绝过头(体验问题),而默认可忽略会让同样的疏忽静默恢复出残缺会话(安全事故)。架构保证了这条规则成立:模型可见内容只经四种带 surfaceOp 标记的 surface 事件加 request/header、request/context 折叠进入重建,危险的未知事件恰好是那些不进 surface 但改变日志其余部分解读方式的事件(session/end-seed 是现存例子)。

已接受的机器记录保持不可变。 已接受兼容性检查点保留已接受的声明与 schema。后续变更获得新确认,升版本决策需要自身的递增头版本转换。检查点不改变未知事件协议。

写入器与发布真源

SESSION_FORMAT_VERSION 拥有工作区写入器版本号;发布状态参考拥有唯一的双语 latestReleasedVersion 与 evidenceTag 记录。发布状态独立于源码开发而变化,因此通过比较这两个事实推导状态,而不另行维护 released 布尔值。一般文档链接到这些真源;固定版本约定与历史证据保留明确版本号。

文档标准检查在不访问网络的情况下,校验记录结构、双语一致性、证据链接一致性及本地发布版本与写入器版本的大小关系。它证明内部一致性,而非发布事实或记录新鲜度。发布流程要求发布操作者在更高格式交付后核实发布并更新记录。这让兼容性评审不依赖凭据与 GitHub 可用性,同时明确人工维护新鲜度的义务。

影响

v0(0812 发布)交付的内容:分方向的拒绝并带原始日志路径;基于生成的已知词汇清单(KNOWN_SESSION_EVENT_TYPES,由 gen-persistence-catalog 从所有 SessionEventMap 声明合并生成,verify-persistence-catalog 保证新鲜)的未知事件守卫;ignorable 信封字段被种子校验、JSONL 和 BFF 线上 schema 接受。V1 添加静态相邻 catalog、恒等 v0-to-v1 迁移边、仅 header descriptor、精确代际 JSONL 发布与已发布 Session 格式定义的当前专用恢复。历史 Session 只读迁移准备负责内存恢复与写入发布之间的 JSONL 时序。V2 让物理 codec 对普通事件词汇与 payload 新增项保持中立:相邻迁移边冻结 released source 与 target 清单,同版本恢复则应用已安装的 known-event set 与当前 payload 语义。第一方 writer 不通过 Session.append 设置 ignorable,而一个仓库外插件仍依赖该字段;同版本保留由外部插件保留决策定义,更严格的历史规则由 alpha 迁移拒绝决策定义。未知类型守卫仍只在读取侧生效,因为 append 时的词汇拒绝会中断活跃 Session 的持久化。JSONL 会在当前 header 或事件解析前从最小原始 header 分类外来版本,因此结构完全不同的未来格式会报告升级方向而不是"损坏"。

曾考虑的替代方案

  • 大小两级版本号:能否转换这一位信息属于每一步的升级器,把它预先固化进编号形状会做出错误承诺。
  • 未知事件默认可忽略:把忘写标记的后果从可见的过度拒绝反转成静默损坏。
  • 在仅 header 列表期间迁移:让便宜清单改变存储,而且需要读取事件正文才能计算 header 无法证明的事实。列表返回 descriptor,事件正文读取负责发布。
  • 插件运行时注册已知事件类型:不予采用,因为该方案会让已知集依赖插件组合,而且只注册事件名称,无法判定省略事件是否安全。持久化的 ignorable 标记把该分类保留在每条记录中;外部插件保留决策定义当前消费方约束。
  • 重复发布标记或运行时状态服务:为不控制 Session 执行的维护信息增加另一个可变真源。写入器常量与发布记录已经足够。
  • 依赖网络的文档门禁或发布自动化:网络查询会把本地文档检查耦合到凭据与 GitHub 可用性;记录一致性不需要运行时服务或发布工作流变更。核实发布仍是发布操作者的明确义务。