# Agent Note: 读取时要求会话事件类型已知

Status: implemented

[English](2026-08-25-fail-closed-session-event-vocabulary.md) | 中文

## 问题

会话读取器不得静默省略自己无法理解的持久事件。未知事件可能改变后续请求重建、策略状态、恢复或其他插件所有的投影，因此 JSON 解析成功不足以证明读取保真。[问题 #1901](https://github.com/deepseek-ai/deepseek-harness/issues/1901) 之前的读取器会放行未知事件类型，而核心折叠会忽略它们，使恢复的会话可能在没有诊断的情况下丢失语义。

最初的拒绝机制将生成的已知事件集合与可选的逐记录 `ignorable: true` 声明结合，该声明原本用于信息性新增事件。没有任何生产写入方使用该声明，`Session.append()` 也没有暴露设置方式。机制落地后新增的事件类型仍然都是读取必需项。但这个未使用字段仍然扩大了权威事件类型、seed 校验、持久化格式、SQLite schema、会话传输、DeepSeek 请求扩展、生成目录、文档与测试。

## 决策

每个会话事件类型都是读取必需项。受支持的 legacy 记录归一化后，`PersistenceCoordinator` 会将每个事件类型与 `KNOWN_SESSION_EVENT_TYPES` 比较；后者是从本仓库声明的所有 `SessionEventMap` 成员生成的集合。任何未知类型都以 `SessionFormatUnsupportedError` 拒绝重建；诊断会列出事件与序号，指明日志可能由更新的写入方生成，并在后端拥有独立原始产物时附上该路径。该守卫仍只在读取侧生效，因为在实时事件已提交后拒绝追加会中断持久化，使会话无法在下次加载时报告不受支持的日志。

`SessionEvent` 没有可选的未知事件跳过字段。JSONL 继续序列化相同的事件对象，因为生产追加路径从未发出该字段，`SESSION_FORMAT_VERSION` 仍为 `0`。SQLite 提供方将被复用的 `ignorable` 列替换为 schema 18 的 `is_packed` 判别值：标量逻辑事件存储 `0`，打包分片行存储 `1`，与物理分片标签同名的事件在协调器应用已知类型守卫之前仍可明确解码。

`SESSION_FORMAT_VERSION` 仍是单个单调整数。当较旧运行时无法完全正确地解释某项结构或语义变更时，写入方必须升版本：会话 header 字段、事件 envelope 字段、核心事件语义或 `SurfaceEventType`/`SurfaceOp` 机制。仅新增事件类型无需升版本，因为较旧读取器会拒绝该确切的未知类型，而不是误读日志。版本相等时正常读取；版本不等时当前以分方向诊断拒绝。n→n+1 升级器链仍推迟到第一个真实 v0→v1 步骤提供可测的输入和输出时建立。未来的查看升级属于内存转换，只有用户继续会话时才持久替换；缺失的步骤会保留源产物以供原始查看。

仓库外的 `SessionEventMap` 成员仍不在生成集合内。它们可在实时进程中运行并持久化，但第一方持久化读取器在重新加载时会拒绝它们，直到真实的外部事件消费方证明需要注册机制。这保留了现有的预发布显式限制，同时避免已知集合依赖运行时组合。

## 考虑过的替代方案

**保留逐记录跳过声明。**不予采用，因为它没有生产使用方，无法通过 `Session.append()` 表达，并且要求每种存储与传输表示都保留一项推测性选择。真实需求应先定义可安全省略的事件类型，再让追加实现统一发出该分类，而不是依赖每个调用点。

**忽略每个未知事件。**不予采用，因为读取器无法推断一项未知持久事实是否仅用于信息。静默省略可能使会话以错误的模型输入或插件状态恢复。

**为每个新事件类型升级会话格式。**不予采用，因为生成的类型守卫已使较旧读取器在确切的不受支持记录处安全失败，而较新读取器仍可接受较旧日志。格式整数仍保留给会改变已知记录解读方式的变更。

**从已挂载插件注册已知事件名称。**在没有当前外部消费方时不予采用，因为同一构建会根据运行时组合接受或拒绝同一份存储日志。未来的注册设计必须区分必需插件状态与真正可选的记录，并将该区分持久保存。

**使用主版本／次版本或在查看时改写。**不予采用，因为升级可用性是每个版本步骤的属性，不是两个计数器编码的承诺；打开会话也不得破坏性地改写其唯一产物。转换器缺陷不得让浏览变成数据丢失，也不得仅因较新运行时查看过日志就使较旧运行时失去访问权。

## 后果

较旧构建在较新的同版本日志包含任何未知事件类型后都无法恢复该日志，即使新事件仅用于信息。这是对未使用的前向降级行为的有意放弃，换取单一事件 envelope 与单一失败规则。如果真实生产方以后需要较旧读取器跳过可选事件并继续会话，设计必须只对事件类型分类一次，让 `Session.append()` 自动发出持久分类，并覆盖两个持久化后端和线上表示。

第一方 JSONL 会话字节保持不变，包括打包行与 `SESSION_FORMAT_VERSION = 0`。现有第一方 JSONL 会话仍可读。SQLite 是可选功能，并遵循预发布 schema 策略：schema 18 不从 schema 17 迁移，不兼容数据库会被拒绝而不是改写。[SQLite 物理压缩决策](../architecture/2026-08-18-sqlite-physical-chunk-row-compression.zh.md)拥有该后端的打包行表示。

组装后的 headless 拒绝测试证明用户会看到未知类型、序号、更新写入方方向与原始 JSONL 路径。核心 seed 测试拒绝当前事件 envelope 以外的字段；持久化约定测试拒绝每个未知类型；SQLite codec 与差分测试覆盖标量与打包判别、后缀读取、修复与跨后端逻辑相等。生成的持久化目录与已知事件模块使读取器集合与仓库所有的声明保持同步。
