Files
deepseek-harness/packages/session/session-persistence/src/index.ts
T
Tianyi Cui 7e445c3a67 refactor(session): fold the session family into packages/session/
git mv the 12 packages from session-persistence/, session-projection/,
session-title/, and telemetry/ into one session/ group per the
regrouping RFC; merge the four group READMEs into one bilingual
triplet; rewrite the group segment in tsconfig references (intra-group
references shorten to ../<pkg>), tsconfig.base.json paths/globs,
knip.json keys, vitest include, gate scripts, and authored doc/note
citations; regenerate module graph, doc graphs, catalogs, and the
lockfile importer keys. No npm names change.

Full unit suite: 8779 passed; the 18 reported failures reproduce as
env flakes (ambient-proxy IPv6 tunneling, watched-dir inotify
timeouts under parallel load) — each passes in isolation with
NO_PROXY set, matching their known pre-existing behavior on master.
2026-08-09 01:21:11 +08:00

204 lines
9.2 KiB
TypeScript

/**
* Durable session-persistence seam (`ctx.sessionPersistence`). Backends store
* {@link SessionEvent}s as the event-sourced log and carry non-replayable
* {@link SessionHeader} metadata separately.
* @module @deepseek-ai/dsh-session-persistence
*/
import { Context, Service } from 'cordis'
import { SessionPreparation } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionPersistenceRevision } from './revision.ts'
// Re-export the metadata vocabulary so consumers import it from the seam.
export type { SessionHeader } from '@deepseek-ai/dsh-session'
export { SessionPersistenceRevision } from './revision.ts'
/** Lightweight immutable source identity returned without loading a full log. */
export interface SessionPersistenceSnapshot {
/** Detached metadata for one materialized session. */
header: SessionHeader
/** Opaque source-qualified token that changes whenever this stored log changes. */
revision: SessionPersistenceRevision
}
/** Immutable logical session prepared from persistence or a live owner. */
export interface SessionInspection {
/** Validated immutable session metadata. */
readonly meta: SessionHeader
/** Validated contiguous logical event log. */
readonly events: readonly SessionEvent[]
}
// The backend-agnostic write-path orchestration first-party backends compose.
export {
DEFAULT_PREPARED_SESSION_CACHE_SIZE,
DEFAULT_WRITE_BATCH_MAX_DELAY_MS,
MAX_WRITE_BATCH_DELAY_MS,
PersistenceCoordinator,
SessionPersistenceCorruptionError,
} from './coordinator.ts'
export type {
PersistenceBackend,
PersistenceCoordinatorOptions,
StoredPrefix,
StoredSuffix,
} from './coordinator.ts'
declare module 'cordis' {
interface Context {
sessionPersistence: SessionPersistence
}
}
/**
* A backend-resolved, per-session local artifact location. The path is an
* absolute target path and can name an artifact that has not materialized yet.
* Consumers must treat it as a location hint, never as an authorization token.
*/
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 append-only session storage. Implementations preserve contiguous,
* losslessly JSON-serializable events; {@link append} resolves only after
* durability, and {@link load} balances a complete interrupted tail without
* rewriting committed events.
*/
export abstract class SessionPersistence extends Service {
constructor(ctx: Context) {
super(ctx, 'sessionPersistence')
}
/**
* Resolve this backend's independent local artifact for a session without
* reading, creating, flushing, or otherwise materializing it. Backends such
* as SQLite that do not own one artifact per session return `undefined`.
* @param meta - the immutable session header whose artifact is requested.
* @returns the backend-specific absolute location, when one exists.
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a
* created-but-never-appended session is absent from {@link list}
* — abandoned sessions leave nothing behind.
* @param meta - the immutable header (id, version, cwd, lineage) to record.
*/
abstract create(meta: SessionHeader): Promise<void>
/**
* Durably persist a batch of events. Honors the append-only and contiguous-
* seq contracts: the first event's `seq` MUST equal the stored next-seq
* (after `load` has durably closed any interrupted turn). Rejects non-JSON-
* serializable `event.data` with an error naming the offending event type.
* @param id - the session the batch belongs to.
* @param events - the contiguous batch to persist, in seq order.
*/
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
/**
* Prepare the exact unpublished Session used by resume. Implementations may
* reuse object graphs retained by an earlier {@link inspect} after confirming
* their durable revision is still current; disposal releases an unpublished
* reservation. Revision retries require the durable log to remain unchanged
* for one read/check round trip; continuous external writers may delay completion.
* @param id - persisted session to prepare.
* @param signal - optional cancellation for preparation work.
* @returns one owned unpublished Session preparation.
*/
async prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation> {
signal?.throwIfAborted()
const loaded = await this.load(id)
signal?.throwIfAborted()
const sessions = this.ctx.get('sessions')
if (sessions === undefined) {
throw new Error('cannot prepare a session: SessionStore is not configured')
}
return SessionPreparation.create(sessions.prepare(id, {
seed: loaded.events.map(event => structuredClone(event)),
meta: structuredClone(loaded.meta),
seedSource: 'persistence',
}))
}
/**
* Load an immutable balanced logical view and commit any required cold
* recovery. A complete interrupted final turn is preserved and durably
* closed with missing tool errors plus any open step and turn boundaries;
* only a torn final record is discarded. Unknown versions and corruption in
* the committed prefix reject. Implementations MUST NOT crash-repair an
* identity still bound to a live Session: a balanced live log may return as a
* durable snapshot, while an open live turn rejects. Returned values may be
* shared with immutable live or prepared state and must not be mutated.
* Revision-based implementations may wait for one stable read/check round trip.
* @param id - the persisted session to reload.
* @returns the header and a log ending on a balanced `turn/end`.
*/
abstract load(id: SessionId): Promise<SessionInspection>
/**
* Inspect an immutable logical session without committing recovery or
* publishing it. A cold complete interrupted turn receives synthetic closers
* in memory and a torn physical tail remains untouched. An already-live
* Session instead yields its current immutable snapshot, which may contain an
* open turn and its `session/end-seed` boundary. Coordinator-backed
* implementations retain the exact cold unpublished Session for bounded
* reuse by a later {@link prepare}. A stale ready source is reloaded; a source
* already committing or reserved for resume remains exclusive, and inspection
* may borrow its immutable view. Callers borrow only the immutable header and
* log. Continuous external writers may delay revision convergence.
* @param id - the persisted session to inspect.
* @param signal - optional cancellation for queued and backend read work.
* @returns the validated header and current logical event log.
*/
abstract inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection>
/**
* Read the stored events from `fromSeq` onward — the read-from-seq
* primitive for read models that resume from a watermark (e.g. a persisted
* projection cache folding only the tail past its checkpoint). Unlike
* {@link inspect}, it is a detached physical suffix read: no preparation
* cache, torn-tail truncation, synthetic closers, or coordinator-state
* publication. Only events from the valid contiguous stored prefix are
* returned, so a torn fragment never reaches the caller. `fromSeq` at or
* beyond the stored prefix returns an empty event list (never an error).
* Backends whose medium can seek by seq
* (SQLite) read only the suffix; sequential media (JSONL, both encodings)
* still parse the whole artifact and skip forward — the primitive bounds
* what is RETURNED and refolded, not every backend's physical read.
* @param id - the persisted session to read.
* @param fromSeq - first event seq to include; a non-negative safe integer.
* @param signal - optional cancellation for queued and backend read work.
* @returns the header and the stored events with `seq >= fromSeq`.
*/
abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal):
Promise<{ meta: SessionHeader; events: SessionEvent[] }>
/**
* Lightweight listing from metadata, without a full-log parse.
* @param signal - optional cancellation for backend listing work.
* @returns one header per materialized session.
*/
abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
/**
* List materialized sessions with cheap per-log change tokens.
*
* Repeated observations of an unchanged log return the same revision. A
* successful mutating {@link load} repair changes the next listed revision.
* Revisions also distinguish independently backed stores so backend-local
* counters cannot compare equal across different persistence sources.
* @param signal - optional cancellation for backend snapshot-listing work.
* @returns one header and opaque revision per materialized session without loading full logs.
*/
abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
}
export default SessionPersistence