Files
deepseek-harness/packages/session/session-persistence/src/handle.ts
T
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

107 lines
4.7 KiB
TypeScript

/**
* The per-session storage handle: one open channel onto a stored session's
* append-only event log, returned by `SessionPersistence.create`/`open`.
* @module @deepseek-ai/dsh-session-persistence/handle
*/
import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
/**
* Log access granted by an open. `write` is read-write: the session's single
* mutator, which also reads its own log. `read` only observes — it never
* takes ownership and works while another handle or process holds `write`.
*/
export type SessionAccess = 'read' | 'write'
/** Options for {@link SessionHandle.read}. */
export interface SessionHandleReadOptions {
/** Optional cancellation for backend read work. */
readonly signal?: AbortSignal
}
/** Options for {@link SessionHandle.append}. */
export interface SessionHandleAppendOptions {
/** Optional cancellation observed before the write starts. */
readonly signal?: AbortSignal
}
/** Options for {@link SessionHandle.flush}. */
export interface SessionHandleFlushOptions {
/** Optional cancellation observed before the barrier starts. */
readonly signal?: AbortSignal
}
/**
* One open channel onto a stored session. A handle is single-owner state, not
* a shared service: `read` never backtracks below what this handle already
* observed, a `write` handle reads its own successful appends, and `close()`
* is the one teardown (idempotent, uncancellable; `Symbol.asyncDispose`
* delegates to it). Every operation on a closed handle rejects with
* `SessionHandleClosedError`.
*
* Freshness across handles: once an `append` or `flush` resolves on a write
* handle, every read STARTED afterwards on the same backend instance — on any
* handle, or through `stat`/`list` — observes at least that prefix.
* Reads concurrent with a mutation carry no ordering promise beyond the valid
* contiguous prefix.
*/
export interface SessionHandle extends AsyncDisposable {
/** The stored session this handle addresses. */
readonly id: SessionId
/** The immutable stored header, fixed at `create`/`open`. */
readonly header: SessionHeader
/**
* Exact fork-inherited prefix length stored with the log; `0` when
* `header.isSeeded` is false. Storage metadata paired with the header for
* every body read, never part of the replayable event log.
*/
readonly inheritedEventCount: SessionLogOffset
/** Whether this handle may mutate the log. */
readonly access: SessionAccess
/**
* Read a slice of the valid contiguous logical log. The slice is a legal log
* prefix segment: a torn physical tail is never returned, and repeated reads
* on this handle never observe an older state than a prior read.
* @param offset - first logical event seq to include; defaults to `0`.
* @param length - maximum number of events to return; defaults to the rest
* of the log. An offset at or past the end returns an empty list.
* @param options - optional cancellation.
* @returns the events with `seq >= offset`, at most `length` of them.
*/
read(offset?: number, length?: number, options?: SessionHandleReadOptions): Promise<readonly SessionEvent[]>
/**
* Append a contiguous batch continuing the current logical end. The first
* event's `seq` MUST equal the stored next-seq; committed events are never
* rewritten. Persistence is best-effort: on resolution the batch is
* accepted, ordered, and visible to reads on this backend instance, but
* only a resolved {@link flush} promises it survives a crash — a backend
* may buffer or batch physical writes behind append. Rejects with
* `SessionReadOnlyError` on a read handle and `SessionOwnershipLostError`
* when write ownership is gone.
* @param events - the contiguous batch, in seq order.
* @param options - optional cancellation observed before the write starts.
*/
append(events: readonly SessionEvent[], options?: SessionHandleAppendOptions): Promise<void>
/**
* The durability barrier — the one operation that promises storage: on
* resolution every acknowledged append is durable and the session is
* materialized for other processes; an empty created session becomes
* durably listable here. Callers that must survive a crash flush; a backend
* whose `append` already persists on resolution treats this as
* materialize-if-needed. Rejects with `SessionReadOnlyError` on a read
* handle.
* @param options - optional cancellation observed before the barrier starts.
*/
flush(options?: SessionHandleFlushOptions): Promise<void>
/**
* Release the handle: a read handle frees local resources; a write handle
* completes pending durability and releases write ownership. Idempotent,
* asynchronous, and deliberately not cancellable.
*/
close(): Promise<void>
}