mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
SessionPersistence grows readFrom(id, fromSeq, signal?): the non-mutating read-from-seq primitive for checkpoint consumers (the persisted projection cache folds only the tail past its watermark). Coordinator owns validation, per-id serialization, and the sequential fallback (loadStored + forward skip); SQLite implements the optional seek-capable loadStoredFrom hook (WHERE seq >= ?), JSONL stays sequential by contract. Contract suite covers suffix exactness, empty-tail, non-mutation, and cancellation; seam README (both languages) documents the method and the hook.
158 lines
7.5 KiB
TypeScript
158 lines
7.5 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 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
|
|
}
|
|
|
|
// The backend-agnostic write-path orchestration first-party backends compose.
|
|
export { PersistenceCoordinator } from './coordinator.ts'
|
|
export type { PersistenceBackend, 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>
|
|
|
|
/**
|
|
* Load a header and balanced contiguous log. 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 with its stored header as a durable snapshot, while an
|
|
* open live turn rejects.
|
|
* A coordinator-backed cold load reserves the identity across storage awaits,
|
|
* so concurrent publication of a same-id live Session rejects.
|
|
* Returned events are detached, and every identified message is deeply
|
|
* frozen. Coordinator-backed implementations upgrade supported pre-identity
|
|
* message events before validation; other malformed messages reject before
|
|
* any stored event is returned.
|
|
* @param id - the persisted session to reload.
|
|
* @returns the header and a log ending on a balanced `turn/end`.
|
|
*/
|
|
abstract load(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
|
|
|
|
/**
|
|
* Inspect a header and its valid contiguous stored prefix without repairing
|
|
* a torn tail, closing an interrupted turn, or publishing coordinator state.
|
|
* This read is serialized with writes for the same id and returns detached
|
|
* values with upgraded, deeply frozen identified messages, so observers
|
|
* cannot mutate message identity/content or backend-owned state. Other
|
|
* malformed messages reject.
|
|
* @param id - the persisted session to inspect.
|
|
* @param signal - optional cancellation for queued and backend read work.
|
|
* @returns the header and valid stored event prefix exactly as observed.
|
|
*/
|
|
abstract inspect(id: SessionId, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
|
|
|
|
/**
|
|
* 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). Like
|
|
* {@link inspect} it is non-mutating and detached: no torn-tail truncation,
|
|
* no synthetic closers, no 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
|