Load sessions persisted before message identity
加载消息标识机制引入前持久化的会话
The identified immutable message change replaced four durable event payloads with complete message values. Existing v0 JSONL and SQLite sessions still held the immediately preceding shapes: direct `content`/`source` on user and steering events, `content`/`provenance` on assistant events, and `callId`/`content`/`isError` on tool results. Their headers still matched `SESSION_FORMAT_VERSION`, but current-shape validatio
English
Problem
The identified immutable message change replaced four durable event payloads with complete message values. Existing v0 JSONL and SQLite sessions still held the immediately preceding shapes: direct content/source on user and steering events, content/provenance on assistant events, and callId/content/isError on tool results. Their headers still matched SESSION_FORMAT_VERSION, but current-shape validation rejected them before resume could construct a live Session.
Changing the message representation without a version bump made those logs indistinguishable at the header level from current v0 logs. The runtime needs a narrow import rule that restores data created by the supported first-party backends without weakening validation for unrelated obsolete or malformed events.
Decision
PersistenceCoordinator normalizes the four exact pre-identity message payloads after backend decoding and before current message validation. It wraps their existing semantic fields in the current role-specific message shape and assigns legacy-message:<session-id>:<event-seq> as the deterministic imported MessageId. A legacy tool/result content replacement inherits the imported id of its replacement target, preserving the current content-only rewrite invariant.
The same normalization runs for load, inspect, an ownerless loaded state claiming its live session, and HMR prefix adoption. Prefix comparisons therefore compare the live current-shape seed with the same normalized stored view. Current-looking wrappers with missing or invalid fields are not repaired, and unsupported event vocabulary, request headers, versions, and surface relations retain their existing rejection paths.
The upgrade is read-only. Stored legacy records remain unchanged; a resumed session appends only current-shape events after them. Deterministic identities make repeated loads and a mixed legacy/current log reproduce the same message ids without a backend-specific rewrite transaction.
Alternatives considered
Reject the logs under the pre-release compatibility stance. This is the default for unrelated v0 churn, but it strands real first-party sessions even though every old field maps unambiguously to the current message representation.
Rewrite the complete stored log in place. This would canonicalize the artifact but violate the append-only storage contract, require separate atomic replacement mechanisms for JSONL and SQLite, and expand a read compatibility fix into a migration system.
Mint random ids on each load. The messages would satisfy the type shape but lose stable identity across inspect, resume, restart, and mixed legacy/current appends.
Consequences
Pre-identity JSONL and SQLite sessions resume with their original message content, sources, assistant provider/model fields, tool correlation, errors, metadata, and surface replacements. The returned events are otherwise indistinguishable from current imported message snapshots and remain deeply frozen.
This is one explicit same-version import exception, not a general v0 compatibility layer. Adding another exception requires another complete, unambiguous mapping at the persistence boundary; malformed current data continues to fail rather than being guessed into validity. The shared coordinator contract exercises the upgrade against the in-memory reference, JSONL, and SQLite backends, including deterministic reload and tool-result replacement identity.
Related
- Create every message as an identified immutable value — owns the current message identity and immutability contract.
- Session persistence as an abstract service — owns the append-only backend and resume boundary.
中文
问题
带标识的不可变消息变更将四种持久化事件载荷替换为完整消息值。现有的 v0 JSONL 和 SQLite 会话仍保留紧邻该变更之前的形状:用户事件和 steering(中途引导)事件直接携带 content/source,assistant 事件携带 content/provenance,工具结果则携带 callId/content/isError。这些会话的标头仍与 SESSION_FORMAT_VERSION 匹配,但当前形状验证会拒绝它们,导致恢复流程无法构造活跃的 Session。
消息表示改变时没有提升版本,导致这些日志无法仅凭标头与当前的 v0 日志区分。运行时需要一条范围受限的导入规则,既能恢复受支持的第一方后端所创建的数据,又不削弱对无关过时事件或格式错误事件的验证。
决策
PersistenceCoordinator 会在后端解码之后、当前消息验证之前,规范化消息标识机制引入前的四种特定消息载荷。它将载荷现有的语义字段包装进当前按角色区分的消息形状,并为其分配确定性的导入用 MessageId:legacy-message:<session-id>:<event-seq>。旧版 tool/result 的内容替换会继承替换目标导入后的 id,从而保持当前仅改写内容的不变量。
同一项规范化也用于 load、inspect、无 owner 的已加载状态认领其活跃会话,以及 HMR(热模块替换)前缀接管。因此,前缀比较会将活跃会话的当前形状 seed 与同一份规范化存储视图进行比较。看似当前形状、但字段缺失或无效的包装层不会被修复;不受支持的事件词汇、请求 header、版本和 surface 关系仍沿用现有拒绝路径。
这项升级只发生在读取时。存储中的旧版记录保持不变;会话恢复后,只会在其后追加当前形状的事件。确定性标识使重复加载以及新旧形状混合的日志无需执行后端专用的重写事务,也能复现相同的消息 id。
考虑过的替代方案
按照预发布兼容性立场拒绝这些日志。 这是处理其他 v0 形状变动的默认方式,但即使每个旧字段都能明确映射到当前消息表示,它仍会导致真实的第一方会话无法恢复。
就地重写完整的存储日志。 这会使产物规范化,但违反仅追加存储约定,还需要为 JSONL 和 SQLite 分别实现原子替换机制,并将一次读取兼容性修复扩大为迁移系统。
每次加载时随机生成 id。 这些消息会满足类型形状,却无法在检查、恢复、重启以及新旧形状混合追加之间保持稳定标识。
后果
消息标识机制引入前的 JSONL 和 SQLite 会话可以恢复,并保留原始的消息内容、来源、assistant 的提供方/模型字段、工具调用关联、错误、元数据和 surface 替换。除此之外,返回事件与当前导入的消息快照无法区分,并且仍然经过深度冻结。
这是一个显式的同版本导入例外,而非通用的 v0 兼容层。若要增加另一个例外,必须在持久化边界提供另一套完整且无歧义的映射;当前数据若格式错误,系统仍会拒绝,而不会猜测如何将其变成有效数据。共享的协调器约定会在内存参考实现、JSONL 和 SQLite 后端上验证这项升级,包括重新加载时的确定性,以及工具结果替换时的标识继承。
相关
- 将每条消息创建为带标识的不可变值:该记录负责当前的消息标识与不可变性约定。
- 会话持久化作为抽象服务:该记录负责仅追加后端与恢复边界。