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

138 lines
5.7 KiB
TypeScript

/**
* Stable failures exposed by the session-persistence service and its handles,
* including the format refusals shared by every backend: a stored log this
* build cannot faithfully interpret is refused, never misread, and the
* refusal points at the raw artifact when the backend keeps one per session.
* @module @deepseek-ai/dsh-session-persistence/errors
*/
import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
/** The requested Session identity has no durable log visible to this caller. */
export class SessionPersistenceNotFoundError extends Error {
/** @param sessionId - absent durable Session identity. */
constructor(readonly sessionId: SessionId) {
super(`session "${sessionId}" not found`)
this.name = 'SessionPersistenceNotFoundError'
}
}
/** `create` targeted a Session identity that already exists in this backend. */
export class SessionAlreadyExistsError extends Error {
/** @param sessionId - the occupied durable Session identity. */
constructor(readonly sessionId: SessionId) {
super(`session "${sessionId}" already exists`)
this.name = 'SessionAlreadyExistsError'
}
}
/** A write open found the session already bound to an active write handle. */
export class SessionAlreadyOwnedError extends Error {
/** @param sessionId - the session whose write ownership is taken. */
constructor(readonly sessionId: SessionId) {
super(`session "${sessionId}" is already owned by an active write handle`)
this.name = 'SessionAlreadyOwnedError'
}
}
/** A mutation (`append`/`flush`) was called on a read handle. */
export class SessionReadOnlyError extends Error {
/**
* @param sessionId - the session the read handle observes.
* @param operation - the refused mutating operation name.
*/
constructor(readonly sessionId: SessionId, operation: string) {
super(`session "${sessionId}": ${operation} is not available on a read handle`)
this.name = 'SessionReadOnlyError'
}
}
/**
* A write handle's ownership is permanently gone: its lease expired, a renewal
* failed, or the durable ownership record no longer names this handle. The
* handle never re-acquires ownership — close it and reopen for write.
*
* Declared for the cross-process lease layer; the shipped in-process backends
* never throw it yet.
*/
export class SessionOwnershipLostError extends Error {
/** @param sessionId - the session whose write ownership this handle lost. */
constructor(readonly sessionId: SessionId) {
super(`session "${sessionId}": write ownership was lost; close this handle and reopen`)
this.name = 'SessionOwnershipLostError'
}
}
/** An operation was called on a handle after `close()` was called. */
export class SessionHandleClosedError extends Error {
/**
* @param sessionId - the session the closed handle addressed.
* @param operation - the refused operation name.
*/
constructor(readonly sessionId: SessionId, operation: string) {
super(`session "${sessionId}": ${operation} on a closed handle`)
this.name = 'SessionHandleClosedError'
}
}
/**
* A backend-resolved, per-session local artifact location. Carried only by
* refusal diagnostics ({@link SessionFormatUnsupportedError}) so a user can
* find the raw log a build refused to interpret; it is not a consumer-facing
* query — log access goes through a session handle's `read`.
*/
export interface SessionLocation {
/** Backend-specific artifact kind, for example `jsonl`. */
readonly kind: string
/** Absolute path to this session's backend-owned artifact. */
readonly path: string
}
/** Durable session contents failed validation after a successful backend read. */
export class SessionPersistenceCorruptionError extends Error {
/**
* @param message - stable corruption context.
* @param options - original validation failure.
*/
constructor(message: string, options: ErrorOptions) {
super(message, options)
this.name = 'SessionPersistenceCorruptionError'
}
}
/**
* The stored log is intact but this runtime cannot faithfully interpret it:
* the header carries an unsupported format version, or an event's type is
* unknown to this build. Distinct from {@link SessionPersistenceCorruptionError}
* — nothing is damaged; the raw log remains readable at {@link location} when
* the backend keeps one artifact per session.
*/
export class SessionFormatUnsupportedError extends Error {
/**
* @param message - stable reason the log cannot be interpreted, already
* including the raw-log path when one exists.
* @param location - the backend's artifact location, when one exists.
*/
constructor(message: string, readonly location?: SessionLocation) {
super(message)
this.name = 'SessionFormatUnsupportedError'
}
}
/**
* Direction-aware refusal text for a stored session whose format version this
* build does not read. Shared by load-time checks and by backends that must
* refuse BEFORE decoding version-dependent structure (a future format may not
* satisfy this build's structural checks at all, and the user must see
* "upgrade the harness", never "corrupt").
* @param id - the stored session id, for message context.
* @param version - the stored format version.
* @returns the stable refusal text, without a raw-log path suffix.
*/
export function sessionFormatVersionRefusal(id: string, version: number): string {
return version > SESSION_FORMAT_VERSION
? `session "${id}" uses log format v${version}, but this harness reads only v${SESSION_FORMAT_VERSION}: the log was written by a newer harness — upgrade the harness to open it`
: `session "${id}" uses log format v${version}, older than the supported v${SESSION_FORMAT_VERSION}, and this build ships no upgrade path for it`
}