deepseek-harness/.agents/notes/implemented/architecture/2026-08-05-session-preparation.md
Turtle bec6805d6a refactor(session-persistence)!: handle-based seam with a lifecycle-owned write path
The persistence seam is now create/open/stat/list returning per-session
SessionHandles (read/append/flush/close); every log read and write flows
through the owning handle. The seam package exports only the service and
handle contracts, consumer-visible errors, and pure durable-data
validation helpers; each backend owns its complete storage runtime, and
the shared contract suites pin equivalent observable behavior. The
backend routes published sessions' live events by id into the active
write handle; agent-loop only acquires, seeds, and closes the handle.
Resume appends interruptedTurnClosers through its write handle;
session-query owns the revision-keyed cold cache. Legacy-only surfaces
are removed in the same swap: locate/readRaw/supportsRawArtifacts, the
legacy event-shape read migration, zstd torn-frame salvage,
DSH_SESSION_JSONL, and hook transcript_path population; a torn final
zstd frame is discarded whole; the session-list cold blank probe returns
on stat metadata (eventCount derived from the last physical row,
sizeBytes). The WebUI ZIP export serializes the logical log from a read
handle, so both backends export identically.

Refs #3245
2026-09-01 23:19:02 +08:00

4.8 KiB

Agent Note: Reusable Session preparation before publication

Status: implemented

English | 中文

Problem

Fresh creation and persisted resume reached the same publication boundary through different construction flows. This obscured the invariant that setup must finish against one unpublished Session before that exact Session and its Agent become visible together.

Cold history inspection and Agent resume also independently materialized the same persisted session log, which this note originally answered with a persistence-side prepared-Session cache; that half is superseded below.

Decision

SessionPreparation owns one exact unpublished Session until publication or rollback. It is a Session lifecycle object, not an Agent lifecycle or activation object. Fresh creation wraps the result of SessionStore.prepare(); persisted resume reads the stored log through the session's write handle, appends interruptedTurnClosers, and wraps SessionStore.prepare(id, { seed, meta, seedSource: 'persistence' }) — the restoration branch that validates and freezes the transferred graphs in place.

The Agent loop consumes both forms through one setup-and-publication pipeline: it acquires the preparation, builds the private Agent context around preparation.session, awaits optional setup, publishes that exact Session and Agent, and disposes the preparation on every exit. Publication transfers the live lifecycle to the existing Session and Agent stores; SessionPreparation itself owns no Agent behavior.

This refines the publication boundary from the Agent lifecycle and ownership decision without replacing its ownership model.

Superseded: the persistence-side preparation lifecycle

This note originally also gave persistence a prepare(id)/inspect(id) lifecycle: a coordinator-backed bounded LRU of cold unpublished Sessions with exclusive reservations, revision-checked reuse, and repair committed inside prepare/load, so history pagination and a later resume shared one cold materialization. The handle-based persistence seam deletes all of it: persistence exposes handles only, resume reads the log through its write handle and owns repair, and read-only observers (session-query) own their cold-Session cache keyed by the stat().revision change token. The read-reuse goal survives in that cache; the exclusive-reservation machinery does not, because the write handle's single-writer ownership is the exclusion resume actually needs. Resume pays one whole-log read through the handle where the prepared cache sometimes served a warm Session — an accepted cost recorded in the handle note.

Boundaries

  • The preparation is one disposable ownership window, not a cache: disposal is synchronous and idempotent, and publication accepts only the exact prepared Session.
  • A fresh create never claims a persisted identity implicitly. Persistence collisions continue to reject (SessionAlreadyExistsError, SessionAlreadyOwnedError).
  • Live Sessions are owned by the existing stores; preparations hold only unpublished ones.

Verification

Agent-loop tests pin the common publication pipeline across create, createAgent, and resume, including rollback on setup failure, cancellation, and teardown, and that disposal releases the write handle (reopening for write succeeds). Session-store tests pin the restoration branch's validate-and-freeze-in-place transfer.

Alternatives considered

Activate an Agent for history reads. Rejected because pagination would keep query-only Agents live and transfer cache retirement into the Agent lifecycle. This rationale still guards the session-query cold cache: observation never creates an Agent.

Cache only { meta, events }. Rejected at the time because resume would still reconstruct a Session from the cached values. Under the handle seam this is exactly what the read side does — session-query caches a cold Session per revision for reads only — while resume rebuilds from the handle read, trading the warm-Session reuse for a single write-ownership door.

Add a restore transaction or coordinator to the Agent loop. Rejected because cold reading and Session construction are persistence and Session concerns. The Agent loop only needs the uniform SessionPreparation ownership boundary; the handle seam kept that split while moving repair to the loop's resume path.

Consequences

Create and resume share one publication protocol without merging Agent and Session responsibilities, and every exit path disposes exactly one preparation. The persistence-side reuse consequences originally recorded here (shared cold materialization, LRU bounds, reservation coordination) now belong to the handle note and the session-query cache that replaced them.