Merge pull request #2722 from deepseek-harness/agent/avoid-persistence-seed-clone

perf(session): 避免重复克隆持久化 seed
This commit is contained in:
Magolor 2026-08-19 17:58:26 +08:00 • committed by GitHub
commit c474d28d0b
5 changed files with 36 additions and 3 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: 93b6cd1bd058499e71948d3909de8e4c076b445e
2026-06-18-shared-persistence-write-coordinator.zh.md: 9e1bc736d4dba5e59b763710976425fa0ae76c30
2026-06-18-shared-persistence-write-coordinator.md: 286bbb7d5cd3720109db0d0abc0bb72ddbfcbdcd
2026-06-18-shared-persistence-write-coordinator.zh.md: d24398c4bc9445739b4cdeb9f2ed176577060932

View file

@ -16,6 +16,10 @@ Composition, not inheritance. The coordinator is a concrete class the backend ho
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.
Creation borrows the exact `Session.events` snapshot as its persistence seed. `Session` has already detached, validated, and deeply frozen every event, and the snapshot array remains stable when later appends replace the cached view. The coordinator and its backend hooks only read this typed in-process value, so cloning the complete log again would duplicate the ownership work described by the [agent-scope runtime decision](2026-07-12-agent-scope-runtime-design.md#session-append-materialize-validate-commit-notify). Public persistence `append()` still snapshots caller-owned input at its API boundary.
Prepared-session suffixes and events admitted to the write-behind queue retain their existing copies. Those paths establish asynchronous queue ownership one suffix or event at a time and have no measured whole-log clone cost; removing their copies remains a separate ownership audit rather than part of creation-seed borrowing.
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.
### The hook interface (`PersistenceBackend<TornMarker>`)

View file

@ -16,6 +16,10 @@ Status: implemented
协调器为每个存活的 `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.events` 的原始快照借作持久化种子。`Session` 已经分离、验证并深度冻结每个事件,后续追加会替换缓存视图,因此该快照数组保持稳定。协调器及其后端钩子只读取这个有类型的进程内值;再次克隆完整日志会重复 [agent scope 运行时决策](2026-07-12-agent-scope-runtime-design.md#session-append-materialize-validate-commit-notify)规定的所有权工作。持久化服务的公开 `append()` 仍在 API 边界为调用方拥有的输入创建快照。
已准备 Session 的后缀,以及进入 write-behind 队列的事件,仍保留现有复制。这些路径会逐个后缀或事件建立异步队列所有权,且没有已测得的完整日志克隆成本;移除这些复制属于单独的所有权审计,不属于创建种子的借用决策。
协调器通过 `session/disposed` 退役会话:它等待控制器完成初始化和当前 flush,串行执行最后一次排空,且仅在成功后才移除控制器与其拥有的每 id 状态。失败时保持控制器可被找到,以供后端 teardown(拆除)重试。每个 id 的已结算链尾仅在其仍是当前链尾时才移除自身,因此旧操作完成后不会抹除同一 id 的新操作。后端 teardown 会注销写入路径监听器、flush 每个剩余的控制器、等待所有按 id 串行化的操作,最后关闭后端。
### 钩子接口(`PersistenceBackend<TornMarker>`)

View file

@ -1170,7 +1170,8 @@ export class PersistenceCoordinator<TornMarker = unknown> {
this.live.set(session, restored)
return restored
}
const seed = session.events.map(e => structuredClone(e))
// Session owns this stable deep-frozen snapshot; backends only serialize it.
const seed = session.events
const live: LiveSessionState = {
init: Promise.resolve(),
writes: this.createWriteBehind(session, () => live.init),

View file

@ -180,6 +180,7 @@ class ControlledBackend implements PersistenceBackend<never> {
readonly name = 'session-persistence-controlled'
readonly store: MemoryStore = new Map()
readonly lifecycle: string[] = []
lastAppendedBatch: readonly SessionEvent[] | undefined
appendAttempts = 0
loadAttempts = 0
repairAttempts = 0
@ -212,6 +213,7 @@ class ControlledBackend implements PersistenceBackend<never> {
}
async appendBatch(m: SessionHeader, events: readonly SessionEvent[], _isMaterialized: boolean): Promise<void> {
this.lastAppendedBatch = events
const attempt = ++this.appendAttempts
await this.beforeAppend?.(attempt)
const entry = this.store.get(m.id)
@ -279,6 +281,28 @@ runCoordinatorContract('memory', async (): Promise<CoordinatorFixture> => {
}
})
describe('PersistenceCoordinator seed ownership', () => {
it('retains the immutable session seed without cloning it', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const backend = new ControlledBackend()
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
new PersistenceCoordinator(inner, backend)
}, { inject: ['sessions'] }))
try {
const session = ctx.sessions.create(SessionId('shared-seed'), { seed: oneTurnLog() })
const seed = session.events
await ctx.sessions.flush(session)
expect(backend.lastAppendedBatch).toBe(seed)
} finally {
await fiber.dispose()
await ctx.fiber.dispose()
}
})
})
describe('PersistenceCoordinator bounded writes', () => {
it('cancels the batching deadline when live initialization rejects', async () => {
vi.useFakeTimers()