Merge branch 'master' into xtr/trajectory-inspection-ui
This commit is contained in:
+6
@@ -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/simplification/2026-07-28-remove-synthetic-log-only-turns.md
|
||||
2026-07-28-remove-synthetic-log-only-turns.md: af4da00f4fe1d7aebff845cd55053bb5b807c979
|
||||
2026-07-28-remove-synthetic-log-only-turns.zh.md: 9d72781d6b7cf396a830790d108f4ff25adc816a
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Agent Note: Remove synthetic turns for log-only events
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-remove-synthetic-log-only-turns.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The session store exposed `appendOutOfBand()` so a plugin could publish a late log-only event while no agent turn was running. The method wrapped that event in `turn/start` and `turn/end`, then flushed it. This preserved the old rule that every durable event had to live inside a turn, but it made one identifier mean both a model-loop execution and a persistence-only update.
|
||||
|
||||
That rule was introduced when persistence recovery treated the last `turn/end` as the only committed boundary. The persistence scanners now preserve every valid contiguous event, and crash repair reacts only to an actually open turn. Idle context already uses the same capability by appending `user/message` between turns. Retaining synthetic turns for title updates therefore inflated turn counts, produced execution outcomes for work that never ran the model, and let a late metadata write consume the next turn number.
|
||||
|
||||
The generic seam also duplicated domain policy. Its marker map said which plugin events were eligible, while the title capability already owned cancellation, liveness, and stale-result rules. Replacing it with another generic or title-specific append wrapper would preserve the same type indirection for two literal event types.
|
||||
|
||||
## Decision
|
||||
|
||||
`SessionStore.appendOutOfBand()`, `OutOfBandSessionEventMap`, and `OutOfBandSessionEventType` do not exist. A plugin that owns a log-only event appends it through `Session`; when the operation promises durability, it explicitly awaits `ctx.sessions.flush(session)`. No turn is opened solely to obtain that checkpoint.
|
||||
|
||||
Core session invariants continue to enforce core-owned execution relations: turn and step numbering, enclosure of steering, assistant, tool, todo, and request-header events, and same-step tool call/result pairing. Core permits merge-extensible events between turns because only their declaring plugin knows whether they are execution-scoped or standalone. Plugin invariant companions remain responsible for their own event relations.
|
||||
|
||||
The title service appends `session/title` directly after its existing service, revision, cancellation, and live-session checks. The bundled model helper appends its literal `session/title-llm-request` record before dispatch. Persistence observes both through the eager `session/event` path and drains them at ordinary checkpoints and lifecycle teardown; neither append forces a flush merely because it is between turns. A fallback, auxiliary request record, or accepted provider title may therefore appear after `turn/end` and before the next `turn/start`.
|
||||
|
||||
A session fork may end at any stable event position outside an open turn, not only at `turn/end`. This preserves standalone title and context records in a default fork while still rejecting a prefix cut through active execution.
|
||||
|
||||
The historical [universal turn-enclosure decision](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md) remains useful only as the reason the synthetic mechanism was introduced. The [context-injection decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) established the current meaning: one turn represents one model-loop execution.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep synthetic zero-step turns.** This preserves a uniform-looking log and reuses `turn/end` as a flush point, but it reports executions that never happened, perturbs turn numbering, and makes every turn consumer filter persistence-only records. Durability already has the independent `session/flush` boundary.
|
||||
|
||||
**Keep a generic core durable-append helper without synthetic turns.** A method that performs `append()` plus `flush()` is small, but its eligibility marker and concurrency promises would still centralize plugin policy in the session store. The event owners already have literal typed append sites, and a caller that truly needs a durability barrier can await the existing `session/flush` operation at that boundary.
|
||||
|
||||
**Store titles as mutable session metadata.** This avoids between-turn events but creates a second mutation, replay, persistence, and fork protocol beside the append-only log. Titles remain replayable latest-wins events instead.
|
||||
|
||||
**Require every plugin event to declare standalone eligibility to core.** This keeps a central allowlist but makes absence mean an execution relation that core cannot verify. Merge-extensible unions already assign semantic ownership to the declaring plugin; its invariant companion is the correct enforcement point.
|
||||
|
||||
## Verification
|
||||
|
||||
Core invariant tests accept an unknown plugin event between turns while continuing to reject built-in execution events there. Hook, compaction, plan-mode, Code Mode dispatch, and approval invariant companions replay existing logs and reject the same execution-scoped events before commit when no turn is open. Session-title service tests pin one direct fallback event under concurrent refresh, detached-session rejection, and newest-revision acceptance. JSONL and SQLite round trips preserve a title appended after `turn/end` through the persistence lifecycle drain, and fork tests retain a standalone log-only tail while rejecting boundaries inside an open turn. A keyless assembled ACP snapshot delays the model-backed title until after `turn/end` and pins one standalone provider title with no synthetic turn. Generated API and type-equivalence catalogs contain no removed symbol.
|
||||
|
||||
## Consequences
|
||||
|
||||
Turn counts and outcomes again describe model-loop executions only. Standalone events consume session seqs, start eager persistence like every other append, and require owners to request an explicit durability barrier only when their operation promises one. Generic plugin mistakes no longer fail under a core default enclosure rule, so each plugin that needs an execution relation must state and test that relation itself. The title capability keeps revision ordering and lifecycle persistence with less core state, no duplicate type seam, and no turn-number collision.
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# 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` 之前。
|
||||
|
||||
会话 fork 可以结束于开放轮次之外的任意稳定事件位置,而不限于 `turn/end`。这样,默认 fork 会保留独立标题和上下文记录,同时仍拒绝在活跃执行过程中截断前缀。
|
||||
|
||||
历史上的[通用轮次封闭决策](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md)如今只适合用于解释为何曾引入合成机制。[上下文注入决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)确立了当前语义:一个轮次表示一次模型循环执行。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留合成的零步骤轮次。** 这可以让日志在形式上保持统一,并复用 `turn/end` 作为刷写点,但会报告从未发生的执行、扰动轮次编号,还会迫使每个轮次消费方过滤仅持久化记录。持久性已有独立的 `session/flush` 边界。
|
||||
|
||||
**保留不使用合成轮次的通用核心持久追加辅助函数。** 执行 `append()` 再执行 `flush()` 的方法本身很小,但其准入标记和并发承诺仍会将插件策略集中到会话存储中。事件所有方已经拥有直接、类型化的字面量追加点;真正需要持久性屏障的调用方可以在该边界等待既有的 `session/flush` 操作。
|
||||
|
||||
**将标题存储为可变会话元数据。** 这可以避免轮次间事件,却会在仅追加日志之外建立第二套变更、回放、持久化和 fork 协议。标题仍采用可回放、后写覆盖的事件。
|
||||
|
||||
**要求每个插件事件向核心声明可独立存在的资格。** 这可以保留中心准入列表,但会让缺少声明意味着一种核心无法验证的执行关系。可合并扩展联合类型已经将语义所有权赋予声明事件的插件;该插件的不变量配套组件才是正确的强制位置。
|
||||
|
||||
## 验证
|
||||
|
||||
核心不变量测试会接受轮次之间的未知插件事件,同时继续拒绝位于该处的内置执行事件。钩子、压缩(compaction)、plan-mode、Code Mode 分发和审批的不变量配套组件会回放既有日志,并在没有开放轮次时,于提交前拒绝相同的执行作用域事件。会话标题服务测试会在并发刷新、会话脱离拒绝和最新修订接受场景下,固定一个直接追加的回退事件。JSONL 和 SQLite 往返测试会通过持久化生命周期排空保留追加在 `turn/end` 之后的标题;fork 测试会保留独立纯日志尾部,同时拒绝位于开放轮次内的边界。一个无密钥、经完整组装的 ACP(Agent Client Protocol)快照会将模型生成的标题延迟到 `turn/end` 之后,并固定一个不含合成轮次的独立提供方标题。生成的 API 和类型等价性目录不含任何已移除符号。
|
||||
|
||||
## 后果
|
||||
|
||||
轮次计数和结果重新只描述模型循环执行。独立事件会占用会话 seq,像其他追加一样启动尽快持久化,并且仅当操作承诺持久性时,才要求事件所有方请求显式持久性屏障。通用插件错误不再因核心默认的封闭规则而失败,因此每个需要执行关系的插件都必须自行声明并测试该关系。标题功能保留修订排序和生命周期持久化,同时减少了核心状态,不再重复类型 seam,并消除了轮次编号冲突。
|
||||
Reference in New Issue
Block a user