# Agent Note: 会话日志读取显式表达物化成本

Status: implemented
Archived: 2026-09-04

[English](2026-08-21-session-log-read-intent.md) | 中文

## 问题

通用的 `Session.events` 访问器会在每次 append 后的首次读取中隐藏一次与数组大小成正比的复制。完整冻结快照可以缓存，但流式输出的每个新事件都会使缓存失效，因此只需要日志长度或单个事件的调用方也可能反复复制数百万个引用。返回值不可变并不要求每种读取意图都承担该成本；事件不可变性由[源端拥有的会话不可变性决策](2026-06-11-dev-invariants-over-deep-readonly.zh.md)另行负责。

## 决策

`Session` 提供三种成本不同的读取操作。`seq` 以常数时间读取当前长度，`eventAt(seq)` 以常数时间读取一个事件，`snapshotEvents(fromSeq?, toSeqExclusive?)` 则为需要数组操作或稳定物化区间的消费方显式物化冻结数组。序列参数是非负日志位置，不是从末尾计算的 `Array.prototype.slice` 偏移量。所选 agent preset 这类重复读取的领域状态来自会话投影，而不是反复扫描活跃日志。

当前完整快照会缓存到下一次 append，使重复读取整个日志的消费方可以共享同一个不可变数组。区间快照只复制所选引用且不缓存：缓存任意区间会保留数量无界的数组，并要求额外的淘汰策略。先前返回的快照保持稳定，因为已接受的事件不可变，且快照数组不会在 append 后增长。

当所需值可以增量维护时，重复的领域状态读取使用[会话投影](2026-08-19-session-projection-state-and-client-views.zh.md)。持久化、导出、回放，以及输出本身就是事件序列的消费方仍适合使用原始日志快照。此 API 使物化成本显式可见，但不会在同一项改动中消除所有完整日志折叠。

## 曾考虑的替代方案

**保留缓存的 `events` 数组访问器。** 这保留了普通数组语法，但会让标量读取和索引读取看似廉价，而一次 append 就可能使其中任何一次读取变成完整日志复制。

**返回提供类数组遍历操作的自定义不可变 cut。** 捕获长度可以在持续增长的日志上提供稳定且常数时间的 cut，但随着调用方要求更多操作，该抽象会不断重新实现所选数组语义。显式索引读取、显式物化和投影以更小的公开 API 覆盖了已经交付的读取意图。

**缓存每个物化区间或维护增量分片快照。** 区间缓存需要保留与淘汰规则，而公开的分片表示会改变消费方和序列化方式；许多调用方已通过索引读取或投影避开这项成本。若实测的完整快照消费方证明有必要，仍可考虑这些表示。

## 后果

- 长度读取和单事件读取不会复制日志。
- 完整快照和区间快照仍有与所选事件数量成正比的显式线性成本。
- 消费方在调用点选择原始历史或增量维护的状态。
- 公开 API 不模拟数组；调用方只在需要数组操作时物化。
