feat(session): bound persistence write batching

This commit is contained in:
Tianyi Cui
2026-08-08 15:43:52 +08:00
parent 7ab20890e6
commit 924c954469
52 changed files with 763 additions and 126 deletions

View File

@@ -2,5 +2,5 @@
# 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/architecture/2026-06-18-shared-persistence-write-coordinator.md
2026-06-18-shared-persistence-write-coordinator.md: 66b73b60ceec9497f1f1226747b8cebd831eb426
2026-06-18-shared-persistence-write-coordinator.zh.md: 424ce6ec7384e8af7b979a29f58c31379a1d1850
2026-06-18-shared-persistence-write-coordinator.md: 12131ebf8380fb8ba816618f0cbaf72cb004623a
2026-06-18-shared-persistence-write-coordinator.zh.md: a11926b602651d8a7dc641f61371e882685b508b

View File

@@ -14,7 +14,7 @@ Extract a backend-agnostic `PersistenceCoordinator` into `dsh-session-persistenc
Composition, not inheritance. The coordinator is a concrete class the backend holds, not a base class the backend extends. The Agent Note's risk — "a coordinator must not make unusual backends fight an inheritance hierarchy" — is avoided: a backend exposes only the hooks and cannot reach the coordinator's private orchestration state. A third-party backend MAY still implement the abstract service directly without the coordinator, including immutable logical inspection and the default preparation fallback through `load`.
The coordinator holds one controller for each exact live `Session`; the controller combines initialization, pending events, and the shared flush promise. Each `session/event` starts an eager drain, and `session/flush` observes quiescence rather than initiating the ordinary write path. The [flush-controller simplification](../simplification/2026-07-23-collapse-persistence-flush-state.md) owns this lifecycle.
The coordinator holds one lifecycle entry for each exact live `Session`: initialization plus a package-private write controller that owns pending events, a fixed batching deadline, the active write, failure retention, and the shared flush barrier. Each `session/event` enters that bounded write path, and `session/flush` bypasses the wait to observe quiescence. The [flush-controller simplification](../simplification/2026-07-23-collapse-persistence-flush-state.md) owns controller consolidation; the [bounded batching decision](2026-08-08-bounded-session-persistence-write-batching.md) owns scheduling cadence.
The coordinator retires a session from `session/disposed`: it waits for the controller's initialization and current flush, serializes a final drain, and removes the controller and owned per-id state only after success. A failure leaves the controller discoverable for backend teardown to retry. Settled per-id chain tails remove themselves only when they are still current, so a completion cannot erase a newer operation for the same id. Backend teardown unregisters write-path listeners, flushes every remaining controller, awaits per-id operations, and then closes the backend.
@@ -35,7 +35,7 @@ The single design choice that keeps the seam clean: the crash-repair "where is t
## Testing
The shared `runPersistenceContract` (public-API contract) runs for every backend and proves that `inspect` balances an interrupted logical view without changing storage or revisions before `prepare` or `load` commits recovery. `runCoordinatorContract` (`tests/coordinator-contract.ts`) covers adoption, HMR, collision, session and backend disposal drains, and crash-tail repair through an in-memory reference, JSONL, and SQLite. `persistence.spec.ts` and `preparations.spec.ts` cover preparation reuse and reservation, bounded prepared-state eviction, eager follow-up batches, live-controller cleanup, same-id chain-tail races, failed-drain retry, and close ordering. The per-backend specs retain storage mechanics only. A through-coordinator torn-tail repair test per real backend keeps the opaque-marker branch covered because the contract crash case produces synthetic closers without a torn marker.
The shared `runPersistenceContract` (public-API contract) runs for every backend and proves that `inspect` balances an interrupted logical view without changing storage or revisions before `prepare` or `load` commits recovery. `runCoordinatorContract` (`tests/coordinator-contract.ts`) covers adoption, HMR, collision, session and backend disposal drains, and crash-tail repair through an in-memory reference, JSONL, and SQLite. `persistence.spec.ts`, `preparations.spec.ts`, and `write-behind.spec.ts` cover preparation reuse and reservation, bounded prepared-state eviction, fixed-window follow-up batches, live-controller cleanup, same-id chain-tail races, failed-batch retry, and close ordering. The per-backend specs retain storage mechanics only. A through-coordinator torn-tail repair test per real backend keeps the opaque-marker branch covered because the contract crash case produces synthetic closers without a torn marker.
## Alternatives considered
@@ -44,4 +44,4 @@ The shared `runPersistenceContract` (public-API contract) runs for every backend
## Consequences
The coordinator adds one indirection, an opaque torn marker, detached session-retirement tasks, and bounded prepared Session state, but centralizes correctness-heavy orchestration previously duplicated by every backend. Session disposal remains an observe-only event, so the session owner does not await persistence retirement; the coordinator contains failures, preserves pending events in the live controller, and makes backend teardown the quiescence boundary. Its hook surface stays narrow: identity, adoption, collision checks, preparation, and immutable inspection reuse `loadStored`; materialization stays atomic inside `appendBatch`; and listing bypasses the coordinator. Read models use `inspect` rather than `load`, so observing a persisted open turn does not commit interruption closers; the [Session preparation decision](2026-08-05-session-preparation.md) owns reuse, reservation, and publication. New backends implement storage primitives rather than copy the eager write lifecycle.
The coordinator adds one indirection, an opaque torn marker, detached session-retirement tasks, and bounded prepared Session state, but centralizes correctness-heavy orchestration previously duplicated by every backend. Session disposal remains an observe-only event, so the session owner does not await persistence retirement; the coordinator contains failures, preserves pending events in the live controller, and makes backend teardown the quiescence boundary. Its hook surface stays narrow: identity, adoption, collision checks, preparation, and immutable inspection reuse `loadStored`; materialization stays atomic inside `appendBatch`; and listing bypasses the coordinator. Read models use `inspect` rather than `load`, so observing a persisted open turn does not commit interruption closers; the [Session preparation decision](2026-08-05-session-preparation.md) owns reuse, reservation, and publication. New backends implement storage primitives rather than copy the bounded write lifecycle.

View File

@@ -14,7 +14,7 @@ Status: implemented
组合,而非继承。协调器是后端持有的具体类,不是后端继承的基类。本 Agent Note 的风险——「协调器不得让非常规后端与继承层级作斗争」——由此规避:后端只暴露钩子,无法触及协调器的私有编排状态。第三方后端仍然可以完全不使用协调器、直接实现抽象服务,包括不可变逻辑检查,以及通过 `load` 实现的默认准备回退。
协调器为每个存活的 `Session` 实例持有一个控制器;该控制器统合初始化、待处理事件与共享 flush promise。每个 `session/event`会立即启动排空,而 `session/flush` 只观察完全停稳,不会发起常规写入路径。[flush 控制器简化](../simplification/2026-07-23-collapse-persistence-flush-state.md)定义该生命周期
协调器为每个存活的 `Session` 实例持有一个生命周期条目:初始化,加上一个包私有写入控制器,后者负责待处理事件、固定批处理截止时间、活跃写入、失败保留和共享 flush 屏障。每个 `session/event`进入这条有界写入路径,`session/flush` 则绕过等待以观察完全停稳。控制器归并由 [flush 控制器简化](../simplification/2026-07-23-collapse-persistence-flush-state.md)定义;调度节奏由[有界批处理决策](2026-08-08-bounded-session-persistence-write-batching.md)定义
协调器通过 `session/disposed` 退役会话:它等待控制器完成初始化和当前 flush串行执行最后一次排空且仅在成功后才移除控制器与其拥有的每 id 状态。失败时保持控制器可被找到,以供后端 teardown拆除重试。每个 id 的已结算链尾仅在其仍是当前链尾时才移除自身,因此旧操作完成后不会抹除同一 id 的新操作。后端 teardown 会注销写入路径监听器、flush 每个剩余的控制器、等待所有按 id 串行化的操作,最后关闭后端。
@@ -35,7 +35,7 @@ Status: implemented
## 测试
共享的 `runPersistenceContract`(公开 API 契约)为每个后端运行,并证明 `inspect` 会配平被中断的逻辑视图但不改变存储或修订版本,随后由 `prepare``load` 提交恢复。`runCoordinatorContract``tests/coordinator-contract.ts`通过内存参考实现、JSONL 与 SQLite 覆盖接管、HMR、碰撞、会话与后端 dispose 排空和崩溃尾部修复。`persistence.spec.ts``preparations.spec.ts` 覆盖准备复用与预留、有界准备状态淘汰、立即执行的后续批次、存活控制器清理、同 id 链尾竞态、排空失败重试与关闭顺序。各后端自身的测试规格只保留存储机制。每个真实后端都有一个经由协调器的崩溃尾部修复测试,以覆盖不透明 marker 分支,因为契约中的崩溃用例会产生合成 closers却不会产生 torn marker。
共享的 `runPersistenceContract`(公开 API 契约)为每个后端运行,并证明 `inspect` 会配平被中断的逻辑视图但不改变存储或修订版本,随后由 `prepare``load` 提交恢复。`runCoordinatorContract``tests/coordinator-contract.ts`通过内存参考实现、JSONL 与 SQLite 覆盖接管、HMR、碰撞、会话与后端 dispose 排空和崩溃尾部修复。`persistence.spec.ts``preparations.spec.ts``write-behind.spec.ts` 覆盖准备复用与预留、有界准备状态淘汰、固定窗口后续批次、存活控制器清理、同 id 链尾竞态、失败批次重试与关闭顺序。各后端自身的测试规格只保留存储机制。每个真实后端都有一个经由协调器的崩溃尾部修复测试,以覆盖不透明 marker 分支,因为契约中的崩溃用例会产生合成 closers却不会产生 torn marker。
## 曾考虑的替代方案
@@ -44,4 +44,4 @@ Status: implemented
## 后果
协调器增加了一层间接、一个不透明的 torn marker、脱离会话生命周期的退役任务以及有界的已准备 Session 状态,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。会话 dispose 仍是仅观察事件,因此会话所有者不会等待持久化退役;协调器会收容失败、在存活控制器中保留待处理事件,并以后端 teardown 为完全停稳边界。其钩子面保持窄小:标识校验、接管、碰撞检查、准备与不可变检查共用 `loadStored`;物化保持在 `appendBatch` 内原子完成;列举绕过协调器。读模型使用 `inspect` 而非 `load`,因此观察已持久化但仍开放的轮次时不会提交中断 closers复用、预留与发布由 [Session 准备阶段决策](2026-08-05-session-preparation.md)定义。新后端只需实现存储原语,而无需复制立即写入生命周期。
协调器增加了一层间接、一个不透明的 torn marker、脱离会话生命周期的退役任务以及有界的已准备 Session 状态,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。会话 dispose 仍是仅观察事件,因此会话所有者不会等待持久化退役;协调器会收容失败、在存活控制器中保留待处理事件,并以后端 teardown 为完全停稳边界。其钩子面保持窄小:标识校验、接管、碰撞检查、准备与不可变检查共用 `loadStored`;物化保持在 `appendBatch` 内原子完成;列举绕过协调器。读模型使用 `inspect` 而非 `load`,因此观察已持久化但仍开放的轮次时不会提交中断 closers复用、预留与发布由 [Session 准备阶段决策](2026-08-05-session-preparation.md)定义。新后端只需实现存储原语,而无需复制有界写入生命周期。

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/architecture/2026-08-08-bounded-session-persistence-write-batching.md
2026-08-08-bounded-session-persistence-write-batching.md: 46dc612492fa1bfa805f77f865f14b168f52776f
2026-08-08-bounded-session-persistence-write-batching.zh.md: 4aafdd652aad87cfe74a449428cdbbf13312d260

View File

@@ -0,0 +1,59 @@
# Agent Note: Bounded session persistence write batching
Status: implemented
English | [中文](2026-08-08-bounded-session-persistence-write-batching.zh.md)
## Problem
Streaming responses can emit many `assistant/chunk` events in a short interval. The persistence coordinator previously scheduled a backend append as soon as an idle queue received one event. Events arriving while that append was active shared a follow-up batch, but a fast backend could still produce many small durable appends. Each JSONL append creates and syncs a Zstandard frame or raw suffix, while each SQLite append opens and commits a transaction and increments the session revision.
Dropping chunk events or replacing them with assembled messages would reduce logical storage, but it would also change the event log, replay, sequence numbers, timestamps, and provenance. The write-amplification problem does not require that larger semantic change.
### Quantified baseline
Repository fixtures make the logical volume concrete. Decoding the current packed rows in [`goal-multi-turn-actions`](../../../../apps/web/tests/snapshots/goal-multi-turn-actions/session.jsonl) yields 2,098 events: 2,017 chunks (96.1%). Their unpacked JSONL lines occupy 332,647 of 379,225 event bytes (87.7%), while chunk packing reduces the committed file to 89,176 bytes and 182 storage rows, including 23 packed chunk rows. [`permission-policy-context`](../../../../apps/web/tests/snapshots/permission-policy-context/session.jsonl) yields 813 events: 746 chunks (91.8%) and 118,935 of 184,821 unpacked event bytes (64.4%); its packed file is 84,917 bytes and 123 storage rows, including 14 packed rows. These are tracked deterministic fixtures, not a production workload distribution, but they demonstrate why deleting chunks would reduce logical volume and why the existing packed-row layout already removes much of their JSON envelope cost.
SQLite stores one row per logical event, so those same logical logs would retain 2,098 and 813 event rows respectively; batching does not change those counts. JSONL writes one Zstandard frame and fsync per durable append batch, while SQLite performs one transaction and one session-revision increment per batch. Runtime files do not record former append boundaries, so fixture row counts cannot honestly be presented as fsync or transaction counts.
The scheduling bound is deterministic. With an immediately resolving sink, the former immediate controller could issue one append for each event arriving after the previous append completed. A controller test admits 20 events 10 ms apart: the 200 ms fixed window hands all 20 to one append. This is a 20-to-1 reduction for that cadence, not a universal ratio. Sparse events, mandatory flushes, slow prior writes, and different arrival rates produce different batch sizes.
## Decision
The first-party JSONL and SQLite plugins expose `writeBatchMaxDelayMs`, a positive integer no greater than Node's timer limit. Its default is `200`. Each plugin resolves the value at load and passes it to `PersistenceCoordinator`; the coordinator remains the single owner of batching behavior.
Each live Session receives a package-private `SessionWriteBehind`. When its pending queue changes from empty to non-empty, the controller starts one fixed window. Later events join that batch without resetting the deadline: this is bounded coalescing, not debounce. When the deadline expires, the controller hands the complete pending prefix to the existing per-id serialization and `appendBatch` path. At most one write for a Session is active. Events admitted during that write form a new pending prefix with their own fixed deadline; if that deadline expires before the active write completes, the new prefix starts immediately after it.
`writeBatchMaxDelayMs` bounds only the controller's intentional batching wait. Event-loop scheduling, initialization, an earlier serialized operation, and backend I/O can delay durable completion, so the option is not a hard fsync or crash-loss SLA.
`session/flush` cancels any remaining wait and becomes a shared quiescence barrier. It drains the active attempt and every event admitted while the barrier is running before it resolves. Session retirement and backend disposal use that same barrier, so lifecycle teardown never waits for the batching timer. The checkpoint policy continues to place mandatory barriers before model requests and top-level tool side effects.
Every event remains durable in its original order and shape. The controller copies each event on admission; no `assistant/chunk`, `seq`, `time`, surface metadata, or storage record is removed or rewritten. JSONL can therefore encode more events in one append frame, and SQLite can insert more event rows in one transaction, without changing either on-disk format or schema version.
A failed background append restores its complete batch before any newer pending events, reports the failure once, and pauses automatic retry. The next newly admitted event opens a fresh fixed window; an explicit flush, retirement, or disposal retries immediately and surfaces a repeated failure to its caller. This avoids a timer-driven failure loop while preserving the existing recoverable flush boundary.
This decision supersedes only the immediate scheduling cadence in [Collapse live persistence into one flush controller](../simplification/2026-07-23-collapse-persistence-flush-state.md). That note remains authoritative for one controller per live Session, retained failed batches, per-id serialization, retirement, and quiescent disposal. The [shared persistence coordinator](2026-06-18-shared-persistence-write-coordinator.md) remains the owner of the backend hook boundary.
## Alternatives considered
**Do not persist streaming chunk events.** Rejected here: it changes the event-sourced authority and recovery semantics rather than only physical write cadence. The existing [assembled-message rejection](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md) remains the guardrail until a no-information-loss replacement defines replay, fork, provenance, sequence, and crash behavior independently. The [packed-row decision](2026-07-26-packed-chunk-rows-by-default.md) remains the complementary JSONL storage-size optimization.
**Write only at semantic checkpoints.** Rejected: it maximizes batching but makes the ordinary crash-loss window depend on a separately mounted policy. Bounded background writes preserve progress between checkpoints while mandatory flushes keep their stronger ordering contract.
**Debounce from the latest event.** Rejected: a continuously streaming response could postpone its first write indefinitely. A fixed window from the first pending event provides a real upper bound on intentional coalescing wait.
**Implement timers separately in JSONL and SQLite.** Rejected: scheduling, failure retention, flush races, and teardown are backend-neutral lifecycle concerns. Duplicating them would reopen the drift that `PersistenceCoordinator` removed.
## Verification
The controller tests use a fake clock to prove the fixed, non-resetting 200 ms window; immediate and shared flush barriers; events admitted during a barrier; an over-budget tail behind an active write; ordered failure retention; paused automatic retry; and explicit retry of an overlapping background failure. Coordinator tests run the controller through Session notifications, retirement, collision reclamation, and teardown. The JSONL and SQLite suites retain their storage-format, transaction, recovery, and shared persistence-contract coverage.
## Consequences
High-frequency event bursts normally produce fewer durable append operations while preserving the exact logical event count. The reduction depends on arrival rate and backend latency: a burst inside one 200 ms window becomes one batch, while mandatory flushes and sparse events can still produce small batches.
This decision does not cap pending event count or bytes behind a slow backend, and it does not reduce SQLite rows or the decoded logical log. A demonstrated memory bound or logical-retention policy would require its own failure and replay contract rather than another hidden timer rule.
An admitted event can remain only in memory during the configured window, and then while scheduling or backend work is outstanding. Deployments choose a smaller value for a narrower ordinary loss window or a larger value for stronger batching. Explicit durability boundaries remain unchanged and bypass the wait.
The new deep module gives the timer, active write, pending prefix, retry pause, and barrier one owner. `PersistenceCoordinator` retains initialization and identity serialization; backends retain only durable storage primitives. Neither `SESSION_FORMAT_VERSION` nor SQLite `SCHEMA_VERSION` changes.

View File

@@ -0,0 +1,59 @@
# Agent Note: 为会话持久化写入批处理设定上界
Status: implemented
[English](2026-08-08-bounded-session-persistence-write-batching.md) | 中文
## 问题
流式响应可能会在短时间内发出大量 `assistant/chunk` 事件。此前,只要空闲队列收到一个事件,持久化协调器就会立即调度一次后端追加。该追加仍在进行时到达的事件会共用一个后续批次,但如果后端速度很快,仍可能产生大量小规模的持久化追加。每次 JSONL 追加都会创建并同步一个 Zstandard 帧或原始格式后缀,而每次 SQLite 追加都会打开并提交一个事务,同时递增会话修订版本。
丢弃分片事件或用组装后的消息替代它们可以减少逻辑存储量,但也会改变事件日志、回放、序列号、时间戳和来源信息。写放大问题不要求采取这项语义变化更大的方案。
### 量化基线
仓库 fixture测试前置数据让逻辑数据量有了具体依据。对当前 [`goal-multi-turn-actions`](../../../../apps/web/tests/snapshots/goal-multi-turn-actions/session.jsonl) 中的打包行进行解码,可得到 2,098 个事件,其中 2,017 个是分片96.1%)。这些分片解包后的 JSONL 行共 332,647 字节,占全部事件 379,225 字节的 87.7%;分片打包则把仓库中的已提交文件缩小到 89,176 字节和 182 个存储行,其中包括 23 个打包分片行。[`permission-policy-context`](../../../../apps/web/tests/snapshots/permission-policy-context/session.jsonl) 可得到 813 个事件,其中 746 个是分片91.8%);这些分片解包后的 JSONL 行共 118,935 字节,占全部事件 184,821 字节的 64.4%。其打包文件为 84,917 字节,共 123 个存储行,其中包括 14 个打包行。这些是纳入版本控制的确定性 fixture不代表生产工作负载分布但它们说明了删除分片为何会降低逻辑数据量也说明现有打包行布局已经消除了大量 JSON 包装开销。
SQLite 每个逻辑事件存储一行,因此同样的逻辑日志会分别保留 2,098 和 813 个事件行批处理不会改变这些数量。JSONL 每个持久化追加批次会写入一个 Zstandard 帧并执行一次 fsyncSQLite 每个批次会执行一次事务并递增一次会话修订版本。运行时文件不记录原有追加边界,因此不能把 fixture 的存储行数当作 fsync 或事务次数。
调度上界是确定的。当写入端会立即完成每次操作时,原来的即时控制器可能对每个在前一次追加完成后到达的事件分别发起一次追加。一个控制器测试以 10 ms 的间隔接纳 20 个事件200 ms 固定窗口会把全部 20 个事件交给一次追加。对于这种到达节奏,追加次数从 20 次降至 1 次,但这不是普遍比例。稀疏事件、强制 flush、较慢的前序写入和不同到达速率都会产生不同的批次大小。
## 决策
第一方 JSONL 与 SQLite 插件公开 `writeBatchMaxDelayMs`,其值必须是一个不超过 Node 计时器上限的正整数,默认值为 `200`。每个插件都会在加载时解析该值,再传给 `PersistenceCoordinator`;批处理行为仍只由协调器负责。
每个活跃的 Session 都有一个包私有 `SessionWriteBehind`。当其待处理队列从空变为非空时,控制器会启动一个固定窗口。后续事件加入该批次但不会重置截止时间:这属于有界合并,而不是防抖。截止时间到达后,控制器会把完整的待处理前缀交给现有的按 id 串行化机制,并沿 `appendBatch` 路径写入。同一 Session 同时最多有一个活跃写入。该写入期间接纳的事件会形成新的待处理前缀,并拥有自己的固定截止时间;如果该截止时间在活跃写入完成前到期,新前缀会在前一次写入完成后立即开始写入。
`writeBatchMaxDelayMs` 只限制控制器为批处理而主动等待的时间。事件循环调度、初始化、此前的串行化操作和后端 I/O 都可能延后持久化完成时间,因此该选项并不对 fsync 完成时间或崩溃数据丢失提供硬性 SLA。
`session/flush` 会取消剩余等待并充当共享的完全停稳屏障。它会在完成前等待活跃写入尝试并排空屏障运行期间接纳的每个事件。Session 退役与后端 dispose资源释放共用该屏障因此生命周期 teardown 绝不会等待批处理计时器。检查点策略仍会在模型请求与顶层工具副作用之前设置强制屏障。
每个事件仍会按原有顺序和形态持久化。控制器会在接纳时复制每个事件;任何 `assistant/chunk``seq``time`、surface 元数据或存储记录都不会被删除或重写。因此JSONL 可以在一个追加帧中编码更多事件SQLite 可以在一个事务中插入更多事件行,而无需改变任一种磁盘格式或 schema 版本。
后台追加失败后,控制器会把完整批次恢复到所有较新的待处理事件之前,报告一次该失败,并暂停自动重试。随后新接纳的第一个事件会开启新的固定窗口;显式 flush、退役或 dispose 会立即重试,如果故障再次发生,则会向调用方暴露该故障。这可以避免计时器驱动的失败循环,同时保留现有可恢复的 flush 边界。
本决策仅取代[将实时持久化归并到单个刷新控制器](../simplification/2026-07-23-collapse-persistence-flush-state.md)中的即时调度节奏。对于每个活跃 Session 使用一个控制器、保留失败批次、按 id 串行化、退役和完全停稳的 dispose原 Agent Note 仍是权威记录。后端钩子边界仍由[共享持久化协调器](2026-06-18-shared-persistence-write-coordinator.md)定义。
## 备选方案
**不持久化流式分片事件。** 这里不采纳这会改变事件日志作为真源的地位及恢复语义而不只是改变物理写入节奏。在无信息损失的替代方案独立定义回放、fork、来源信息、序列和崩溃行为之前现有的[拒绝仅保留组装消息的决策](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md)仍是防护规则。[打包行决策](2026-07-26-packed-chunk-rows-by-default.md)仍是配套的 JSONL 存储体积优化。
**仅在语义检查点写入。** 不采纳:此方案会最大化批处理,却让普通的崩溃丢失窗口取决于另行挂载的策略。有界后台写入会在检查点之间持久化进度,而强制 flush 继续提供更强的顺序契约。
**按最新事件重置防抖窗口。** 不采纳:持续不断的流式响应可能无限期推迟首次写入。由第一个待处理事件启动的固定窗口,为主动合并等待提供了真正的上界。
**分别在 JSONL 与 SQLite 中实现计时器。** 不采纳调度、失败保留、flush 竞态和 teardown 都是后端无关的生命周期问题。重复实现这些机制会重新引入 `PersistenceCoordinator` 已消除的实现漂移。
## 验证
控制器测试使用假时钟证明固定且不会重置的 200 ms 窗口、即时且可共享的 flush 屏障、屏障运行期间接纳的事件、在活跃写入之后已超过窗口时限的尾部批次、有序保留失败批次、暂停自动重试,以及显式 flush 会重试与其重叠发生的后台失败。协调器测试会在 Session 通知、退役、冲突回收和 teardown 路径中验证该控制器。JSONL 与 SQLite 测试套件继续覆盖存储格式、事务、恢复和共享持久化契约。
## 后果
高频事件突发通常会减少持久化追加操作,同时保持逻辑事件数量完全不变。减少幅度取决于事件到达速率和后端延迟:位于同一 200 ms 窗口内的突发事件会成为一个批次,而强制 flush 与稀疏事件仍可能产生小批次。
本决策不会限制因后端缓慢而积压的待处理事件数量或字节数,也不会减少 SQLite 行数或解码后的逻辑日志。若要建立经过验证的内存上界或逻辑保留策略,就必须为其另行定义失败与回放契约,而不是再引入一条隐式计时器规则。
接纳后的事件在配置窗口内可能只存在于内存中,此后在等待调度或后端工作完成期间也可能如此。部署可以选择较小的值以缩短普通丢失窗口,也可以选择较大的值以加强批处理。显式持久性边界保持不变,并会绕过等待。
新的 deep module 让计时器、活跃写入、待处理前缀、重试暂停和屏障由一个所有方统一负责。`PersistenceCoordinator` 继续负责初始化和按标识串行化;后端仍只负责持久存储原语。`SESSION_FORMAT_VERSION` 与 SQLite `SCHEMA_VERSION` 均不变。

View File

@@ -2,5 +2,5 @@
# 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/feature/2026-07-21-log-backed-session-titles.md
2026-07-21-log-backed-session-titles.md: 81ac687c6f55dd0ca1eaeb9d84c811edcfe17b5c
2026-07-21-log-backed-session-titles.zh.md: a3a8cb6fa4b766657176f2827a3641d4874b834b
2026-07-21-log-backed-session-titles.md: 5f4c53a5fb866663bbfaae6b3fdafb4660424804
2026-07-21-log-backed-session-titles.zh.md: f9629c1109c8428c74270de9475423d29b99e8f1

View File

@@ -18,7 +18,7 @@ The [`session-title` capability family](../../../../packages/session-title/READM
Every accepted revision is a log-only `session/title` event. Its payload contains normalized non-empty text, the exact eligible human `user/message` seqs used to derive it, and either fallback provenance or the registered provider id plus optional provider/model route. Before an auxiliary title-model dispatch, the shared helper appends a log-only `session/title-llm-request` event containing the title-provider id, exact source seqs, route, system prompt, messages, and output-token cap; a later generation failure leaves the request auditable. The dispatched envelope is deep-frozen to preserve exact agreement with that record but carries no process-local agent-loop request identity, so loop-only reconstruction checks do not compare it with the main conversation header. Validation failures that never reach dispatch create no request event. `foldSessionTitle()` selects the latest title event and adds that event's seq and timestamp as `SessionTitleSnapshot`. Neither event enters `session.surface` or `deriveMessages()`.
The title service appends `session/title` directly after checking its current revision and exact live session; the bundled model helper likewise appends its literal `session/title-llm-request` record before dispatch. Both records may sit between turns without inventing an execution boundary. Persistence observes them eagerly and drains through ordinary checkpoints and lifecycle teardown; title publication does not force a per-event flush. No generic marker, cast, or settlement queue sits between the event owner and `Session.append()`. This is the domain-specific application of the [standalone log-only event decision](../simplification/2026-07-28-remove-synthetic-log-only-turns.md).
The title service appends `session/title` directly after checking its current revision and exact live session; the bundled model helper likewise appends its literal `session/title-llm-request` record before dispatch. Both records may sit between turns without inventing an execution boundary. Persistence admits them to bounded background batches and drains through ordinary checkpoints and lifecycle teardown; title publication does not force a per-event flush. No generic marker, cast, or settlement queue sits between the event owner and `Session.append()`. This is the domain-specific application of the [standalone log-only event decision](../simplification/2026-07-28-remove-synthetic-log-only-turns.md).
### Input and asynchronous timing

View File

@@ -18,7 +18,7 @@ Status: implemented
每个已接受的修订都是纯日志 `session/title` 事件。其载荷包含规范化后的非空文本、用于派生标题的所有合格且来源为人类的 `user/message` 的准确 seq以及回退来源信息或已注册的提供方 id 加可选的提供方和模型路由。辅助标题模型发起调用前,共享辅助组件会追加一个纯日志 `session/title-llm-request` 事件,其载荷包含标题提供方 id、准确的源 seq、路由、系统提示词、消息和输出 token 上限;即使后续生成失败,这次请求仍可审计。发送的请求信封经过深度冻结,以确保其与该记录精确一致,但它有意不携带进程本地的 agent loop智能体循环请求身份因此仅针对 agent loop 的重建检查不会将它与主对话请求头进行比较。未进入调用阶段的验证失败不会创建请求事件。`foldSessionTitle()` 选择最新的标题事件,并将该事件的 seq 和时间戳加入 `SessionTitleSnapshot`。这两类事件都不会进入 `session.surface``deriveMessages()`
标题服务会在检查当前修订和确切的实时会话后,直接追加 `session/title`;随附模型辅助函数同样会在发起调用前追加其字面量 `session/title-llm-request` 记录。两类记录都可以位于轮次之间,而无需虚构执行边界。持久化会尽快观察它们,并通过常规检查点和生命周期 teardown 排空;标题发布不会强制逐事件 flush。事件所有方与 `Session.append()` 之间不存在通用标记、类型断言或结算队列。这是[独立纯日志事件决策](../simplification/2026-07-28-remove-synthetic-log-only-turns.md)在特定领域中的应用。
标题服务会在检查当前修订和确切的实时会话后,直接追加 `session/title`;随附模型辅助函数同样会在发起调用前追加其字面量 `session/title-llm-request` 记录。两类记录都可以位于轮次之间,而无需虚构执行边界。持久化会将它们接纳到有界后台批次中,并通过常规检查点和生命周期 teardown 排空;标题发布不会强制逐事件 flush。事件所有方与 `Session.append()` 之间不存在通用标记、类型断言或结算队列。这是[独立纯日志事件决策](../simplification/2026-07-28-remove-synthetic-log-only-turns.md)在特定领域中的应用。
### 输入与异步时序

View File

@@ -2,5 +2,5 @@
# 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/feature/2026-07-28-feedback-command.md
2026-07-28-feedback-command.md: 770c72954d66c9e26e03c47f99c261626e6ed09b
2026-07-28-feedback-command.zh.md: c8bd57ddb5e94bfb826ec40b771547ca9625c8e3
2026-07-28-feedback-command.md: 56660b3796c44d3d510dd2901e6a155498673d88
2026-07-28-feedback-command.zh.md: ec03a5c4a0e96d5350085c3d8d26f2523962f1b5

View File

@@ -16,7 +16,7 @@ The capture surface has to be usable at the moment of annoyance, which rules out
The package declares the log-only `feedback/record { text }` session event and exports `recordFeedback(session, text)` as its command-independent producer. The producer discards surrounding whitespace, rejects an empty result, and appends exactly one event. `/feedback` delegates to it, so another UI, hook, or host integration can record the same domain fact without constructing a slash command.
`dsh-commands` still writes its `command/run` / `command/done` lifecycle pair around `/feedback`, but this command sets `recordInput: false`. Its `command/run` therefore carries the command identity and source without `args`; the feedback text exists only in `feedback/record`, while `command/done` carries the acknowledgement outcome. All three records are log-only and non-surface. Their appends start persistence's ordinary eager drain; nothing forces a flush, so acknowledgement reports that the feedback is in the log rather than already on disk.
`dsh-commands` still writes its `command/run` / `command/done` lifecycle pair around `/feedback`, but this command sets `recordInput: false`. Its `command/run` therefore carries the command identity and source without `args`; the feedback text exists only in `feedback/record`, while `command/done` carries the acknowledgement outcome. All three records are log-only and non-surface. Their appends enter persistence's ordinary bounded write path; nothing forces a flush, so acknowledgement reports that the feedback is in the log rather than already on disk.
Capture remains inert for the running agent and model. The optional OTel telemetry package later adds one infrastructure consumer: it observes `feedback/record` as a release trigger in `FEEDBACK_ONLY` mode and as the local-only warning trigger in `DISABLED` mode, without changing the feedback event or command path. See [Feedback-gated session telemetry](2026-08-05-feedback-gated-session-telemetry.md).

View File

@@ -16,7 +16,7 @@ Status: implemented
本包package声明仅写入日志的 `feedback/record { text }` 会话事件,并导出 `recordFeedback(session, text)`,作为不依赖命令的生产方。该生产方丢弃前后空白,拒绝空结果,并且恰好追加一个事件。`/feedback` 委托给它,因此其他 UI、钩子或 host 集成无需构造斜杠命令也能记录同一个领域事实。
`dsh-commands` 仍会围绕 `/feedback` 写入 `command/run` / `command/done` 生命周期配对,但该命令设置了 `recordInput: false`。因此,它的 `command/run` 携带命令标识与来源,但不携带 `args`;反馈文本只存在于 `feedback/record` 中,而 `command/done` 携带确认结果。三个记录都仅写入日志且非 surface。它们的追加会启动持久化的常规即时排空;没有任何环节强制 flush因此确认文本报告的是反馈已进入日志而非已经落盘。
`dsh-commands` 仍会围绕 `/feedback` 写入 `command/run` / `command/done` 生命周期配对,但该命令设置了 `recordInput: false`。因此,它的 `command/run` 携带命令标识与来源,但不携带 `args`;反馈文本只存在于 `feedback/record` 中,而 `command/done` 携带确认结果。三个记录都仅写入日志且非 surface。它们的追加会进入持久化的常规有界写入路径;没有任何环节强制 flush因此确认文本报告的是反馈已进入日志而非已经落盘。
采集对正在运行的 agent 与模型仍不产生后续动作。可选的 OTel 遥测包后续增加了一个基础设施消费方:它在 `FEEDBACK_ONLY` 模式下将 `feedback/record` 作为释放触发器,在 `DISABLED` 模式下将其作为本地警告触发器,且不改变反馈事件或命令路径。见[反馈门控的会话遥测](2026-08-05-feedback-gated-session-telemetry.md)。

View File

@@ -2,5 +2,5 @@
# 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-23-collapse-persistence-flush-state.md
2026-07-23-collapse-persistence-flush-state.md: a9b0f6847712f47d46adb6b01c57563033738964
2026-07-23-collapse-persistence-flush-state.zh.md: e665d164fb66b2bce97a4f6ded88f2ee07324e61
2026-07-23-collapse-persistence-flush-state.md: 2801079e5b322d076eced3a0113e7958d9b4c3b9
2026-07-23-collapse-persistence-flush-state.zh.md: dfc8fc9576416b1f51f4f9c978f49aee4f39f938

View File

@@ -4,15 +4,17 @@ Status: implemented
English | [中文](2026-07-23-collapse-persistence-flush-state.zh.md)
The [bounded write-batching decision](../architecture/2026-08-08-bounded-session-persistence-write-batching.md) supersedes this note's immediate scheduling cadence. The single-controller ownership, failure retention, per-id serialization, retirement, and quiescent-disposal decisions remain current.
## Problem
The persistence coordinator represented one live session's write lifecycle with separate buffer, initialization, and retirement containers plus the per-id operation chain. Those structures mirrored the same fact: whether that exact `Session` still had initialization or events that must settle before its state could be released. The checkpoint-only drain also kept every event volatile until another plugin requested `session/flush`, even though the backend could begin durability work without blocking the synchronous producer.
## Decision
Each live `Session` has one controller containing `pending`, `init`, and the optional current `flush` promise. A `session/event` listener copies the frozen event into `pending` and immediately schedules `ensureFlush()`. Calls during an active write reuse the same promise. The drain snapshots one stable pending prefix and removes it only after `appendBatch` commits; events admitted during the write remain after that prefix and schedule one follow-up batch.
Each live `Session` has one lifecycle entry containing initialization and one package-private write controller. The controller owns `pending`, the fixed batching timer, the optional active write, automatic-retry pause, and the shared flush barrier. A `session/event` listener copies the frozen event into `pending`; the first event starts a fixed deadline, and later events join without resetting it. A write takes one stable pending prefix; events admitted while it runs remain pending for a separately bounded follow-up batch.
`session/flush` is an observation barrier. It waits for initialization and repeatedly awaits or starts the controller's flush until neither a current promise nor pending events remain. An eager failure is logged without rejecting the synchronous event producer, retains the complete batch, and is retried by the next explicit flush, retirement attempt, or backend teardown. Explicit flush and teardown still surface the failure if that retry rejects.
`session/flush` is an immediate quiescence barrier. It waits for initialization, cancels the batching timer, joins any active attempt, and drains pending events, including events admitted while the barrier runs. A background failure is logged without rejecting the synchronous event producer, restores the complete ordered batch, and pauses automatic retry. A new event starts a fresh fixed window; explicit flush, retirement, or backend teardown retries immediately and surfaces a repeated failure.
Initialization now enters the existing per-id operation chain once and calls the unserialized core operations while it owns that turn. The chain remains separate from the live controller because detached public `create`/`append`/`load` calls can race without a `Session` object and still require identity-level serialization.
@@ -22,17 +24,17 @@ The live-controller map is also the retirement registry. Successful retirement d
## Alternatives considered
**Keep checkpoint-only write-behind.** This can form larger batches, but makes durability depend on a separately mounted checkpoint policy and maximizes the crash-loss window between checkpoints. Eager scheduling still coalesces synchronous bursts and events arriving during an active write.
**Keep checkpoint-only write-behind.** This can form larger batches, but makes durability depend on a separately mounted checkpoint policy and maximizes the crash-loss window between checkpoints. Bounded background scheduling still coalesces bursts and persists progress between mandatory barriers.
**Use one coordinator-wide flush promise.** The attachment pattern works for one file, but a global promise would serialize unrelated sessions. One controller per live session preserves independent backend progress while the per-id chain protects same-identity operations.
**Latch the first eager error permanently.** This makes every later flush deterministic, but prevents the existing teardown retry from recovering a transient storage failure. Retaining the batch without latching the error preserves both observability and retry.
**Latch the first background error permanently.** This makes every later flush deterministic, but prevents the existing teardown retry from recovering a transient storage failure. Retaining the batch without latching the error preserves both observability and retry.
**Reject every live load.** This is safe but removes established balanced live snapshots used by persistence consumers and tests. Snapshot-before-flush gives the call a stable linearization point: successful flush proves exactly that snapshot is durable, while the live path never invokes crash repair.
## Verification
- A focused coordinator test gates the first append, admits another event during that write, and observes an automatic second durable batch without calling `session/flush`.
- Focused controller tests use a fake clock to prove the non-resetting fixed window, gate the first append, admit another event during that write, and observe an automatic second durable batch without calling `session/flush`.
- The shared coordinator contract still covers live adoption, collisions, crash repair, and session/backend disposal over the in-memory, JSONL, and SQLite backends.
- Failure and teardown tests keep rejected batches pending, retry them before close, and prove an in-flight controller delays backend close.
- The shared backend contract persists an open live turn, proves `load` rejects without writing synthetic closers, completes and retires the owner, then reloads the exact completed turn.
@@ -42,6 +44,6 @@ The live-controller map is also the retirement registry. Successful retirement d
## Consequences
The coordinator has three long-lived containers: persisted identity state, live-session controllers, and per-id operation chains. Eager writes reduce the ordinary crash-loss window and remove separate buffer, initialization, and retirement registries. They can produce more backend batches than checkpoint-only draining; same-tick bursts and events admitted during one write still coalesce.
The live-session entry keeps initialization beside one controller for pending events, its timer, active write, retry pause, and flush barrier. Separate coordinator containers retain persisted identity state, prepared cold Sessions, retiring identity waiters, and per-id operation chains because those lifecycles also exist without one writable live Session. Bounded writes reduce the ordinary crash-loss window and produce fewer backend batches than immediate scheduling, while mandatory barriers remain unchanged.
`session/flush` no longer chooses when ordinary persistence begins. It remains the ordering and error-observation boundary used by the loop and checkpoint policy, so a successful checkpoint still means every event admitted before its completion is durable.

View File

@@ -4,15 +4,17 @@ Status: implemented
[English](2026-07-23-collapse-persistence-flush-state.md) | 中文
[有界写入批处理决策](../architecture/2026-08-08-bounded-session-persistence-write-batching.md)取代了本 Agent Note 中的即时调度节奏。单控制器归属、失败保留、按 id 串行化、退役和完全停稳的资源释放决策仍然有效。
## 问题
持久化协调器使用彼此独立的缓冲区、初始化容器和退役容器,以及按 id 划分的操作链,表示一个活跃会话的写入生命周期。这些结构反映的是同一个事实:该 `Session` 是否仍有初始化操作或事件必须完成,之后才能释放其状态。仅由检查点触发的排空还会让每个事件都停留在易失状态,直至另一个插件请求 `session/flush`,尽管后端可以在不阻塞同步生产方的情况下开始持久化工作。
## 决策
每个活跃的 `Session` 都有一个控制器,其中包含 `pending``init` 和可选的当前 `flush` promise。`session/event` 监听器将冻结的事件复制到 `pending`,并立即调度 `ensureFlush()`。活跃写入期间的调用复用同一个 promise。排空操作会对待处理事件中一个稳定的前缀生成快照并且只在 `appendBatch` 提交后移除该前缀;写入期间接纳的事件留在该前缀之后,并调度一个后续批次。
每个活跃的 `Session` 都有一个生命周期条目,其中包含初始化和一个包私有写入控制器。该控制器负责 `pending`、固定批处理计时器、可选的活跃写入、自动重试暂停和共享 flush 屏障。`session/event` 监听器将冻结的事件复制到 `pending`;第一个事件设定固定截止时间,后续事件加入但不会重置。一次写入会取出一个稳定的待处理前缀;写入期间接纳的事件留在待处理队列中,并形成另一个独立有界的后续批次。
`session/flush`观测屏障。它等待初始化完成,并反复等待或启动控制器的刷新,直至当前 promise 和待处理事件均不存在。即时写入失败会被记录,但不会拒绝同步事件生产方;完整批次会保留下来,由下一次显式刷新、退役尝试或后端资源销毁重试。若该次重试仍失败,显式刷新和资源销毁仍会向调用方暴露失败。
`session/flush`即时完全停稳屏障。它等待初始化完成、取消批处理计时器、等待任何活跃尝试完成,并排空待处理事件,包括屏障运行期间接纳的事件。后台写入失败会被记录,但不会拒绝同步事件生产方;系统会恢复完整且顺序不变的批次,并暂停自动重试。新事件会开启新的固定窗口;显式 flush、退役或后端资源销毁会立即重试,并在失败再次发生时向调用方暴露失败。
初始化只进入现有的按 id 操作链一次,并在占有该轮执行权时调用未串行化的核心操作。该操作链与活跃控制器保持分离,因为公共 `create``append``load` 调用即使没有 `Session` 对象仍可能发生竞态,依然需要按标识串行执行。
@@ -22,17 +24,17 @@ Status: implemented
## 备选方案
**保留仅由检查点触发的延后写入。** 这种方式可以形成更大的批次,但会让持久性依赖另行挂载的检查点策略,并使检查点之间因崩溃而丢失数据的窗口达到最大。即时调度仍会合并同步突发事件,以及活跃写入期间到达的事件
**保留仅由检查点触发的延后写入。** 这种方式可以形成更大的批次,但会让持久性依赖另行挂载的检查点策略,并使检查点之间因崩溃而丢失数据的窗口达到最大。有界后台调度仍会合并突发事件,并在强制屏障之间持久化进度
**在整个协调器范围内使用一个刷新 promise。** 这种挂接方式适用于单个文件,但全局 promise 会串行化互不相关的会话。每个活跃会话各有一个控制器,既能让不同会话的后端操作独立推进,又由按 id 操作链保护同一标识的操作。
**永久锁存首次即时写入错误。** 这会让后续每次刷新都得到确定的结果,却会阻止现有的资源销毁重试从暂时性存储故障中恢复。保留批次但不锁存错误,可以同时保留可观测性和重试能力。
**永久锁存首次后台写入错误。** 这会让后续每次刷新都得到确定的结果,却会阻止现有的资源销毁重试从暂时性存储故障中恢复。保留批次但不锁存错误,可以同时保留可观测性和重试能力。
**拒绝对所有活跃会话的加载。** 这样做很安全,但会让持久化消费方和测试无法再使用既有的闭合活跃会话快照。先生成快照再刷新,为调用提供了稳定的线性化点:刷新成功即可证明正是该快照已持久化,而活跃路径绝不调用崩溃修复。
## 验证
- 一个针对协调器的测试会阻塞第一次追加,在该次写入期间接纳另一个事件,并在不调用 `session/flush` 的情况下观测到自动执行的第二个持久批次。
- 针对控制器的测试使用假时钟证明固定窗口不会重置,随后阻塞第一次追加,在该次写入期间接纳另一个事件,并在不调用 `session/flush` 的情况下观测到自动执行的第二个持久批次。
- 共享协调器契约仍覆盖内存、JSONL 和 SQLite 后端上的活跃会话接管、冲突、崩溃修复,以及会话和后端的资源释放。
- 失败和资源销毁测试会让写入失败的批次保持待处理,在关闭前重试这些批次,并证明尚在执行的控制器会延迟后端关闭。
- 共享后端契约会持久化一个仍打开的活跃轮次,证明 `load` 会拒绝且不会写入合成闭合事件,随后完成该轮次并让其所有者退役,最后重新加载完全相同的已完成轮次。
@@ -42,6 +44,6 @@ Status: implemented
## 后果
协调器有三个长生命周期容器:持久化的标识状态、活跃会话控制器和按 id 操作链。即时写入缩短了通常情况下因崩溃而丢失数据的窗口,并移除了彼此独立的缓冲区、初始化注册表和退役注册表。与仅由检查点触发的排空相比,这种方式可能产生更后端批次;同一轮事件循环内的突发事件和一次写入期间接纳的事件仍会合并
活跃会话条目把初始化与一个控制器放在一起;该控制器负责待处理事件、计时器、活跃写入、重试暂停和 flush 屏障。协调器仍以独立容器保存已持久化的标识状态、准备好的冷态 Session、标识退役等待方和按 id 操作链,因为即使不存在可写的活跃 Session这些生命周期仍然存在。有界写入缩短了通常情况下因崩溃而丢失数据的窗口相比即时调度产生更少的后端批次,同时不改变强制屏障
`session/flush` 不再决定普通持久化何时开始。它仍是循环和检查点策略使用的顺序与错误观测边界,因此检查点成功仍表示在其完成前接纳的每个事件都已持久化。

View File

@@ -2,5 +2,5 @@
# 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: 720aa6bbe246ea960a61f6d6f67d36d1f85af08a
2026-07-28-remove-synthetic-log-only-turns.zh.md: 587165a58ae322192b9ed1a9b3312f61e0b21ce6
2026-07-28-remove-synthetic-log-only-turns.md: 61c1aa4b1599fe020e0cd96c32daea01d419e95a
2026-07-28-remove-synthetic-log-only-turns.zh.md: 1661045ca9fb340ebffc9b749263262cb178eae4

View File

@@ -18,7 +18,7 @@ The generic seam also duplicated domain policy. Its marker map said which plugin
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`. Manual compaction uses the same between-turn capability for a `compact/* { turn: null }` bracket, but explicitly flushes the closed attempt because `/compact` promises durability before releasing queued prompt admission.
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 admits both through the bounded `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`. Manual compaction uses the same between-turn capability for a `compact/* { turn: null }` bracket, but explicitly flushes the closed attempt because `/compact` promises durability before releasing queued prompt admission.
A session fork may end at any stable event position outside an open turn, not only at `turn/end`. This preserves standalone title and other plugin-owned log-only records in a default fork while still rejecting a prefix cut through active execution.
@@ -40,4 +40,4 @@ Core invariant tests accept an unknown plugin event between turns while continui
## Consequences
Turn counts and outcomes again describe model-loop executions only. Standalone events and manual compaction brackets consume session seqs without consuming a turn number, 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, and manual compaction gains durable control with no synthetic-turn or turn-number collision.
Turn counts and outcomes again describe model-loop executions only. Standalone events and manual compaction brackets consume session seqs without consuming a turn number, enter bounded 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, and manual compaction gains durable control with no synthetic-turn or turn-number collision.

View File

@@ -18,7 +18,7 @@ Status: implemented
核心会话不变量继续强制核心所属的执行关系轮次与步骤编号、steering、助手、工具、待办和请求头事件的封闭以及同一步骤内的工具调用结果配对。核心允许可合并扩展事件位于轮次之间因为只有声明它们的插件知道这些事件受执行作用域约束还是可以独立存在。插件的不变量配套组件仍负责其自身的事件关系。
标题服务会在完成既有的服务状态、修订、取消和实时会话检查后,直接追加 `session/title`。随附模型辅助函数会在发起调用前追加其字面量 `session/title-llm-request` 记录。持久化通过尽快处理的 `session/event` 路径观察两者,并在常规检查点与生命周期 teardown 时排空;二者都不会仅因为位于轮次之间就强制 flush。因此回退标题、辅助请求记录或已接受的提供方标题可以出现在 `turn/end` 之后、下一个 `turn/start` 之前。手动压缩compaction利用同一项轮次间能力记录 `compact/* { turn: null }` 标记对,但会显式 flush 已闭合的尝试,因为 `/compact` 承诺在释放排队提示词接纳预留前完成持久化。
标题服务会在完成既有的服务状态、修订、取消和实时会话检查后,直接追加 `session/title`。随附模型辅助函数会在发起调用前追加其字面量 `session/title-llm-request` 记录。持久化通过有界 `session/event` 路径接纳两者,并在常规检查点与生命周期 teardown 时排空;二者都不会仅因为位于轮次之间就强制 flush。因此回退标题、辅助请求记录或已接受的提供方标题可以出现在 `turn/end` 之后、下一个 `turn/start` 之前。手动压缩compaction利用同一项轮次间能力记录 `compact/* { turn: null }` 标记对,但会显式 flush 已闭合的尝试,因为 `/compact` 承诺在释放排队提示词接纳预留前完成持久化。
会话 fork 可以结束于开放轮次之外的任意稳定事件位置,而不限于 `turn/end`。这样,默认 fork 会保留独立标题和其他插件所属的纯日志记录,同时仍拒绝在活跃执行过程中截断前缀。
@@ -40,4 +40,4 @@ Status: implemented
## 后果
轮次计数和结果重新只描述模型循环执行。独立事件和手动压缩标记对会占用会话 seq但不占用轮次编号它们像其他追加一样启动尽快持久化,并且仅当操作承诺持久性时,才要求事件所有方请求显式持久性屏障。通用插件错误不再因核心默认的封闭规则而失败,因此每个需要执行关系的插件都必须自行声明并测试该关系。标题功能保留修订排序和生命周期持久化,同时减少了核心状态;手动压缩则获得持久控制,不产生合成轮次或轮次编号冲突。
轮次计数和结果重新只描述模型循环执行。独立事件和手动压缩标记对会占用会话 seq但不占用轮次编号它们像其他追加一样进入有界持久化,并且仅当操作承诺持久性时,才要求事件所有方请求显式持久性屏障。通用插件错误不再因核心默认的封闭规则而失败,因此每个需要执行关系的插件都必须自行声明并测试该关系。标题功能保留修订排序和生命周期持久化,同时减少了核心状态;手动压缩则获得持久控制,不产生合成轮次或轮次编号冲突。