fix(session): load pre-react-loop persisted sessions

This commit is contained in:
_Kerman
2026-08-04 11:25:31 +08:00
parent 98947afece
commit 2a9ebfcac2
19 changed files with 486 additions and 45 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md
2026-08-04-load-pre-react-loop-sessions.md: da204822251ef646b982683e74413a001a19873a
2026-08-04-load-pre-react-loop-sessions.zh.md: 1c2818fd3afbefb3af91f53514b9cfc2baa01739

View File

@@ -0,0 +1,40 @@
# Agent Note: Load sessions from the pre-react-loop format
Status: implemented
English | [中文](2026-08-04-load-pre-react-loop-sessions.zh.md)
## Problem
The react-loop simplification changed durable events while retaining `SESSION_FORMAT_VERSION` 0. Stored sessions from the change's base contain `steering/message`, a `turn/start.trigger`, and `turn/end` records without the current top-level `step`; their terminal reasons also use coarse `aborted`, separate `disposed`, and two older error payloads. Current surface and turn invariants cannot replay those records directly.
The new durable inbox is not part of this compatibility problem. The base emitted process-local inbox notifications but no `agent/inbox/*` session events, so replaying old history as pending work would resurrect already claimed or discarded prompts.
## Decision
`PersistenceCoordinator` recognizes the exact pre-react-loop shapes after backend decoding and projects them into the current read view. It removes the obsolete `turn/start.trigger`, converts `steering/message` to the same identified `user/message`, adds the last entered step to `turn/end`, maps old failure facts into the current structured error, folds `disposed` into an aborted turn with the `disposed` cause, and represents coarse aborted records with the persistence-only `{ kind: 'legacy' }` cause because their caller is unavailable.
The coordinator applies the projection to `load`, `inspect`, adoption, HMR prefix comparison, and `readFrom`. A seek-capable `readFrom` normally reads only its suffix; when that suffix contains a legacy event needing an earlier step or replacement identity, the coordinator loads and normalizes the complete prefix before returning the requested seq range.
The importer does not synthesize inbox splices. A resumed pre-react-loop agent begins with empty pending lists, matching the base runtime's inability to persist pending inbox work. The stored artifact remains append-only and later events use the current format.
## Alternatives considered
**Treat the same-version records as unsupported.** This follows the pre-release default but strands sessions produced by the PR base even though the removed steering content, turn step, and terminal facts have complete mappings.
**Replay old inbox notifications into durable splices.** Those notifications were not session events and do not provide a trustworthy pending-state snapshot. Inferring insertions without every claim and discard would re-run consumed work.
**Assign coarse aborted records to an existing caller.** Mapping them to `user`, `parent`, or `hook` would manufacture provenance. A dedicated `legacy` cause keeps the stop classification without making a false audit claim.
**Rewrite stored JSONL and SQLite records.** A rewrite would violate the append-only contract and require backend-specific atomic migration machinery for a read compatibility boundary.
## Consequences
Sessions written in the refactor's base format resume through the current AgentLoop with their steering content, turn boundaries, error facts, and stop classification intact. The shared coordinator contract covers in-memory, JSONL, and SQLite `load`/`inspect`/`readFrom`, including the SQLite suffix fallback; an assembled JSONL Agent resume verifies that the historical transcript is visible while both new inbox lists start empty.
This exception supports the base format, not intermediate formats produced during development of the refactor. In particular, it defines no migration for earlier experimental `agent/inbox/spliced` payloads. Exact-shape recognition keeps malformed current-looking records on their rejection path instead of guessing them into validity.
## Related
- [Load sessions persisted before message identity](2026-07-28-load-pre-identity-session-messages.md) — owns deterministic identities and the general read-only import boundary for another same-version format change.
- [Session persistence as an abstract service](../architecture/2026-06-14-session-persistence.md) — owns append-only backend storage and resume.

View File

@@ -0,0 +1,40 @@
# Agent Note: 加载 react-loop 重构前格式的会话
Status: implemented
[English](2026-08-04-load-pre-react-loop-sessions.md) | 中文
## 问题
react-loop 简化在保持 `SESSION_FORMAT_VERSION` 为 0 的同时更改了持久事件。该变更基线所存储的会话包含 steering中途引导事件 `steering/message``turn/start.trigger`,以及缺少当前顶层 `step``turn/end` 记录;其终止原因还使用粗粒度 `aborted`、独立的 `disposed` 和两种旧版错误载荷。当前 surface 和轮次不变量无法直接回放这些记录。
新的持久 inbox 不属于此兼容性问题。该基线会发出进程本地 inbox 通知,但不会产生 `agent/inbox/*` 会话事件,因此将旧历史回放为待处理工作会让已经领取或丢弃的提示词再次执行。
## 决策
`PersistenceCoordinator` 会在后端解码后识别 react-loop 重构前的确切形状,并将其投影为当前读取视图。它移除已废弃的 `turn/start.trigger`,把 `steering/message` 转换为同一条带标识的 `user/message`,为 `turn/end` 补上最后进入的步骤,将旧版失败事实映射为当前结构化错误,把 `disposed` 折叠为带 `disposed` 原因的已中止轮次,并用仅供持久化导入使用的 `{ kind: 'legacy' }` 原因表示粗粒度中止记录,因为无法获得其调用方。
协调器会把该投影应用于 `load``inspect`、接管、HMR 前缀比较和 `readFrom`。可寻址的 `readFrom` 通常只读取后缀;如果后缀包含需要更早步骤或替换标识的旧版事件,协调器会先加载并规范化完整前缀,再返回所请求的 seq 范围。
导入器不会合成 inbox splice。恢复后的 react-loop 重构前 agent 从空的待处理列表开始,这与基线运行时无法持久化待处理 inbox 工作的行为一致。已存储产物仍然仅追加,后续事件使用当前格式。
## 考虑过的替代方案
**将同版本记录视为不受支持。** 这符合预发布阶段的默认立场,但会使 PR 基线产生的会话无法恢复,尽管已移除的 steering 内容、轮次步骤和终止事实都有完整映射。
**将旧 inbox 通知回放为持久 splice。** 这些通知不是会话事件,也无法提供可信的待处理状态快照。如果无法获知每一次领取和丢弃,就推断插入操作,会让已消费的工作再次执行。
**将粗粒度中止记录归因于现有调用方。** 将其映射到 `user``parent``hook` 会虚构来源。专用的 `legacy` 原因既能保留停止分类,也不会产生虚假的审计事实。
**重写已存储的 JSONL 和 SQLite 记录。** 重写会违反仅追加契约,并要求为读取兼容边界建立后端专用的原子迁移机制。
## 后果
以重构基线格式写入的会话可以通过当前 AgentLoop 恢复,并完整保留 steering 内容、轮次边界、错误事实和停止分类。共享协调器契约覆盖内存、JSONL 和 SQLite 的 `load``inspect``readFrom`,包括 SQLite 后缀回退;组装后的 JSONL agent 恢复用例会验证历史 transcript文本记录可见同时两个新 inbox 列表都从空状态开始。
此例外支持基线格式,不支持重构开发期间产生的中间格式。具体而言,它没有为更早的实验性 `agent/inbox/spliced` 载荷定义迁移。通过确切形状识别,当前格式外观相似但结构错误的记录仍会走拒绝路径,不会被猜测性地转换为有效记录。
## 相关资料
- [加载消息标识机制引入前持久化的会话](2026-07-28-load-pre-identity-session-messages.md):负责另一项同版本格式变更的确定性标识和通用只读导入边界。
- [以抽象服务实现会话持久化](../architecture/2026-06-14-session-persistence.md):负责仅追加后端存储和恢复。