index.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * Durable session-persistence seam (`ctx.sessionPersistence`). Backends store
  3. * {@link SessionEvent}s as the event-sourced log and carry non-replayable
  4. * {@link SessionHeader} metadata separately.
  5. * @module @deepseek-ai/dsh-session-persistence
  6. */
  7. import { Context, Service } from 'cordis'
  8. import { SessionPreparation } from '@deepseek-ai/dsh-session'
  9. import type { SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
  10. import type { SessionPersistenceRevision } from './revision.ts'
  11. // Re-export the metadata vocabulary so consumers import it from the seam.
  12. export type { SessionHeader } from '@deepseek-ai/dsh-session'
  13. export { SessionPersistenceRevision } from './revision.ts'
  14. /** Lightweight immutable source identity returned without loading a full log. */
  15. export interface SessionPersistenceSnapshot {
  16. /** Detached metadata for one materialized session. */
  17. header: SessionHeader
  18. /** Opaque source-qualified token that changes whenever this stored log changes. */
  19. revision: SessionPersistenceRevision
  20. }
  21. /** Immutable logical session prepared from persistence or a live owner. */
  22. export interface SessionInspection {
  23. /** Validated immutable session metadata. */
  24. readonly meta: SessionHeader
  25. /** Validated contiguous logical event log. */
  26. readonly events: readonly SessionEvent[]
  27. }
  28. // The backend-agnostic write-path orchestration first-party backends compose.
  29. export {
  30. DEFAULT_PREPARED_SESSION_CACHE_SIZE,
  31. DEFAULT_WRITE_BATCH_MAX_DELAY_MS,
  32. MAX_WRITE_BATCH_DELAY_MS,
  33. PersistenceCoordinator,
  34. SessionPersistenceCorruptionError,
  35. } from './coordinator.ts'
  36. export type {
  37. PersistenceBackend,
  38. PersistenceCoordinatorOptions,
  39. StoredPrefix,
  40. StoredSuffix,
  41. } from './coordinator.ts'
  42. declare module 'cordis' {
  43. interface Context {
  44. sessionPersistence: SessionPersistence
  45. }
  46. }
  47. /**
  48. * A backend-resolved, per-session local artifact location. The path is an
  49. * absolute target path and can name an artifact that has not materialized yet.
  50. * Consumers must treat it as a location hint, never as an authorization token.
  51. */
  52. export interface SessionLocation {
  53. /** Backend-specific artifact kind, for example `jsonl`. */
  54. readonly kind: string
  55. /** Absolute path to this session's backend-owned artifact. */
  56. readonly path: string
  57. }
  58. /**
  59. * Durable append-only session storage. Implementations preserve contiguous,
  60. * losslessly JSON-serializable events; {@link append} resolves only after
  61. * durability, and {@link load} balances a complete interrupted tail without
  62. * rewriting committed events.
  63. */
  64. export abstract class SessionPersistence extends Service {
  65. constructor(ctx: Context) {
  66. super(ctx, 'sessionPersistence')
  67. }
  68. /**
  69. * Resolve this backend's independent local artifact for a session without
  70. * reading, creating, flushing, or otherwise materializing it. Backends such
  71. * as SQLite that do not own one artifact per session return `undefined`.
  72. * @param meta - the immutable session header whose artifact is requested.
  73. * @returns the backend-specific absolute location, when one exists.
  74. */
  75. abstract locate(meta: SessionHeader): SessionLocation | undefined
  76. /**
  77. * Register a new session's metadata. A backend MAY defer the physical write
  78. * until the first {@link append} (lazy materialization), in which case a
  79. * created-but-never-appended session is absent from {@link list}
  80. * — abandoned sessions leave nothing behind.
  81. * @param meta - the immutable header (id, version, cwd, lineage) to record.
  82. */
  83. abstract create(meta: SessionHeader): Promise<void>
  84. /**
  85. * Durably persist a batch of events. Honors the append-only and contiguous-
  86. * seq contracts: the first event's `seq` MUST equal the stored next-seq
  87. * (after `load` has durably closed any interrupted turn). Rejects non-JSON-
  88. * serializable `event.data` with an error naming the offending event type.
  89. * @param id - the session the batch belongs to.
  90. * @param events - the contiguous batch to persist, in seq order.
  91. */
  92. abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
  93. /**
  94. * Prepare the exact unpublished Session used by resume. Implementations may
  95. * reuse object graphs retained by an earlier {@link inspect} after confirming
  96. * their durable revision is still current; disposal releases an unpublished
  97. * reservation. Revision retries require the durable log to remain unchanged
  98. * for one read/check round trip; continuous external writers may delay completion.
  99. * @param id - persisted session to prepare.
  100. * @param signal - optional cancellation for preparation work.
  101. * @returns one owned unpublished Session preparation.
  102. */
  103. async prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation> {
  104. signal?.throwIfAborted()
  105. const loaded = await this.load(id)
  106. signal?.throwIfAborted()
  107. const sessions = this.ctx.get('sessions')
  108. if (sessions === undefined) {
  109. throw new Error('cannot prepare a session: SessionStore is not configured')
  110. }
  111. return SessionPreparation.create(sessions.prepare(id, {
  112. seed: loaded.events.map(event => structuredClone(event)),
  113. meta: structuredClone(loaded.meta),
  114. seedSource: 'persistence',
  115. }))
  116. }
  117. /**
  118. * Load an immutable balanced logical view and commit any required cold
  119. * recovery. A complete interrupted final turn is preserved and durably
  120. * closed with missing tool errors plus any open step and turn boundaries;
  121. * only a torn final record is discarded. Unknown versions and corruption in
  122. * the committed prefix reject. Implementations MUST NOT crash-repair an
  123. * identity still bound to a live Session: a balanced live log may return as a
  124. * durable snapshot, while an open live turn rejects. Returned values may be
  125. * shared with immutable live or prepared state and must not be mutated.
  126. * Revision-based implementations may wait for one stable read/check round trip.
  127. * @param id - the persisted session to reload.
  128. * @returns the header and a log ending on a balanced `turn/end`.
  129. */
  130. abstract load(id: SessionId): Promise<SessionInspection>
  131. /**
  132. * Inspect an immutable logical session without committing recovery or
  133. * publishing it. A cold complete interrupted turn receives synthetic closers
  134. * in memory and a torn physical tail remains untouched. An already-live
  135. * Session instead yields its current immutable snapshot, which may contain an
  136. * open turn and its `session/end-seed` boundary. Coordinator-backed
  137. * implementations retain the exact cold unpublished Session for bounded
  138. * reuse by a later {@link prepare}. A stale ready source is reloaded; a source
  139. * already committing or reserved for resume remains exclusive, and inspection
  140. * may borrow its immutable view. Callers borrow only the immutable header and
  141. * log. Continuous external writers may delay revision convergence.
  142. * @param id - the persisted session to inspect.
  143. * @param signal - optional cancellation for queued and backend read work.
  144. * @returns the validated header and current logical event log.
  145. */
  146. abstract inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection>
  147. /**
  148. * Read the stored events from `fromSeq` onward — the read-from-seq
  149. * primitive for read models that resume from a watermark (e.g. a persisted
  150. * projection cache folding only the tail past its checkpoint). Unlike
  151. * {@link inspect}, it is a detached physical suffix read: no preparation
  152. * cache, torn-tail truncation, synthetic closers, or coordinator-state
  153. * publication. Only events from the valid contiguous stored prefix are
  154. * returned, so a torn fragment never reaches the caller. `fromSeq` at or
  155. * beyond the stored prefix returns an empty event list (never an error).
  156. * Backends whose medium can seek by seq
  157. * (SQLite) read only the suffix; sequential media (JSONL, both encodings)
  158. * still parse the whole artifact and skip forward — the primitive bounds
  159. * what is RETURNED and refolded, not every backend's physical read.
  160. * @param id - the persisted session to read.
  161. * @param fromSeq - first event seq to include; a non-negative safe integer.
  162. * @param signal - optional cancellation for queued and backend read work.
  163. * @returns the header and the stored events with `seq >= fromSeq`.
  164. */
  165. abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal):
  166. Promise<{ meta: SessionHeader; events: SessionEvent[] }>
  167. /**
  168. * Lightweight listing from metadata, without a full-log parse.
  169. * @param signal - optional cancellation for backend listing work.
  170. * @returns one header per materialized session.
  171. */
  172. abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
  173. /**
  174. * List materialized sessions with cheap per-log change tokens.
  175. *
  176. * Repeated observations of an unchanged log return the same revision. A
  177. * successful mutating {@link load} repair changes the next listed revision.
  178. * Revisions also distinguish independently backed stores so backend-local
  179. * counters cannot compare equal across different persistence sources.
  180. * @param signal - optional cancellation for backend snapshot-listing work.
  181. * @returns one header and opaque revision per materialized session without loading full logs.
  182. */
  183. abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
  184. }
  185. export default SessionPersistence