# Agent Note: 移除纯日志事件的合成轮次

Status: implemented

[English](2026-07-28-remove-synthetic-log-only-turns.md) | 中文

## 问题

会话存储曾暴露 `appendOutOfBand()`，让插件可以在没有 agent（智能体）轮次运行时发布延迟到达的纯日志事件。该方法会用 `turn/start` 和 `turn/end` 包住事件，再将其刷写。这保留了「每个持久事件都必须位于轮次内」的旧规则，却让同一个标识符既表示模型循环执行，又表示仅持久化更新。

引入该规则时，持久化恢复曾将最后一个 `turn/end` 视为唯一的已提交边界。如今，持久化扫描器会保留每个合法且连续的事件，崩溃修复也只处理确实处于开放状态的轮次。空闲上下文早已采用同一机制，在轮次之间追加 `user/message`。因此，为标题更新保留合成轮次会夸大轮次计数、为从未运行模型的工作产生执行结果，还会让延迟到达的元数据写入占用下一个轮次编号。

通用 seam 还重复了领域策略。它的标记映射说明哪些插件事件符合条件，而标题功能本就拥有取消、活跃性和陈旧结果处理规则。改用另一个通用或标题专属追加包装层，仍会为两个字面量事件类型保留同一层类型间接性。

## 决策

`SessionStore.appendOutOfBand()`、`OutOfBandSessionEventMap` 和 `OutOfBandSessionEventType` 均不再存在。拥有纯日志事件的插件通过 `Session` 追加该事件；当操作承诺持久性时，插件会显式等待 `ctx.sessions.flush(session)`。系统不会仅为获得该检查点而打开轮次。

核心会话不变量继续强制核心所属的执行关系：轮次与步骤编号、steering、助手、工具、待办和请求头事件的封闭，以及同一步骤内的工具调用／结果配对。核心允许可合并扩展事件位于轮次之间，因为只有声明它们的插件知道这些事件受执行作用域约束，还是可以独立存在。插件的不变量配套组件仍负责其自身的事件关系。

标题服务会在完成既有的服务状态、修订、取消和实时会话检查后，直接追加 `session/title`。随附模型辅助函数会在发起调用前追加其字面量 `session/title-llm-request` 记录。持久化通过尽快处理的 `session/event` 路径观察两者，并在常规检查点与生命周期 teardown 时排空；二者都不会仅因为位于轮次之间就强制 flush。因此，回退标题、辅助请求记录或已接受的提供方标题可以出现在 `turn/end` 之后、下一个 `turn/start` 之前。手动压缩（compaction）利用同一项轮次间能力记录 `compact/* { turn: null }` 标记对，但会显式 flush 已闭合的尝试，因为 `/compact` 承诺在释放排队提示词接纳预留前完成持久化。

会话 fork 可以结束于开放轮次之外的任意稳定事件位置，而不限于 `turn/end`。这样，默认 fork 会保留独立标题和上下文记录，同时仍拒绝在活跃执行过程中截断前缀。

历史上的[通用轮次封闭决策](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md)如今只适合用于解释为何曾引入合成机制。[上下文注入决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)确立了当前语义：一个轮次表示一次模型循环执行。[排队手动压缩决策](../feature/2026-07-30-queued-manual-compaction.md)将该规则应用于持久多事件标记对，并拥有其标记与接纳语义。

## 曾考虑的替代方案

**保留合成的零步骤轮次。** 这可以让日志在形式上保持统一，并复用 `turn/end` 作为刷写点，但会报告从未发生的执行、扰动轮次编号，还会迫使每个轮次消费方过滤仅持久化记录。持久性已有独立的 `session/flush` 边界。

**保留不使用合成轮次的通用核心持久追加辅助函数。** 执行 `append()` 再执行 `flush()` 的方法本身很小，但其准入标记和并发承诺仍会将插件策略集中到会话存储中。事件所有方已经拥有直接、类型化的字面量追加点；真正需要持久性屏障的调用方可以在该边界等待既有的 `session/flush` 操作。

**将标题存储为可变会话元数据。** 这可以避免轮次间事件，却会在仅追加日志之外建立第二套变更、回放、持久化和 fork 协议。标题仍采用可回放、后写覆盖的事件。

**要求每个插件事件向核心声明可独立存在的资格。** 这可以保留中心准入列表，但会让缺少声明意味着一种核心无法验证的执行关系。可合并扩展联合类型已经将语义所有权赋予声明事件的插件；该插件的不变量配套组件才是正确的强制位置。

## 验证

核心不变量测试会接受轮次之间的未知插件事件，同时继续拒绝位于该处的内置执行事件。钩子、plan-mode、Code Mode 分发和审批的不变量配套组件会在没有开放轮次时拒绝其执行作用域事件；压缩配套组件则另外接受轮次之间平衡的 `turn: null` 手动标记对，并要求数字 owner 匹配一个开放轮次。会话标题服务测试会在并发刷新、会话脱离拒绝和最新修订接受场景下，固定一个直接追加的回退事件。JSONL 和 SQLite 往返测试会通过持久化生命周期排空保留追加在 `turn/end` 之后的标题；fork 测试会保留独立纯日志尾部，同时拒绝位于开放轮次内的边界。一个无密钥、经完整组装的 ACP（Agent Client Protocol）快照会将模型生成的标题延迟到 `turn/end` 之后，并固定一个不含合成轮次的独立提供方标题。生成的 API 和类型等价性目录不含任何已移除符号。

## 后果

轮次计数和结果重新只描述模型循环执行。独立事件和手动压缩标记对会占用会话 seq，但不占用轮次编号；它们像其他追加一样启动尽快持久化，并且仅当操作承诺持久性时，才要求事件所有方请求显式持久性屏障。通用插件错误不再因核心默认的封闭规则而失败，因此每个需要执行关系的插件都必须自行声明并测试该关系。标题功能保留修订排序和生命周期持久化，同时减少了核心状态；手动压缩则获得持久控制，不产生合成轮次或轮次编号冲突。
