index.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /**
  2. * Durable session-persistence Service Definition (`ctx.sessionPersistence`). Backends store
  3. * {@link SessionEvent}s as the event-sourced log and carry non-replayable
  4. * {@link SessionHeader} metadata separately; callers address one stored
  5. * session through a {@link SessionHandle} obtained from `create`/`open`.
  6. * @module @deepseek-ai/dsh-session-persistence
  7. */
  8. import { Context, Service } from '@deepseek-ai/cordis'
  9. import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
  10. import type { SessionHandle, SessionAccess } from './handle.ts'
  11. import type { SessionPersistenceRevision } from './revision.ts'
  12. // Re-export the metadata vocabulary so Consumers import it from the Service Definition.
  13. export type { SessionHeader } from '@deepseek-ai/dsh-session'
  14. export { SessionPersistenceRevision } from './revision.ts'
  15. export type {
  16. SessionAccess,
  17. SessionHandle,
  18. SessionHandleAppendOptions,
  19. SessionHandleFlushOptions,
  20. SessionHandleReadOptions,
  21. SessionHandleReadResult,
  22. } from './handle.ts'
  23. export {
  24. SessionAlreadyExistsError,
  25. SessionAlreadyOwnedError,
  26. SessionFormatUnsupportedError,
  27. SessionHandleClosedError,
  28. SessionOwnershipLostError,
  29. SessionPersistenceCorruptionError,
  30. SessionPersistenceNotFoundError,
  31. SessionReadOnlyError,
  32. sessionFormatVersionRefusal,
  33. } from './errors.ts'
  34. export type { SessionLocation } from './errors.ts'
  35. export {
  36. assertContiguous,
  37. assertStoredId,
  38. assertVersion,
  39. materializeAppendBatch,
  40. materializeCreateHeader,
  41. validateStoredEvents,
  42. } from './storage-contract.ts'
  43. /**
  44. * Lightweight stored-session observation returned by {@link SessionPersistence.stat}
  45. * and {@link SessionPersistence.list} without reading the full event log.
  46. */
  47. export interface SessionPersistenceSnapshot {
  48. /** Detached metadata for one stored session. */
  49. readonly header: SessionHeader
  50. /** Opaque change token; see {@link SessionPersistence.stat}. */
  51. readonly revision: SessionPersistenceRevision
  52. /** Logical event count, when the backend can provide it cheaply from metadata; otherwise absent. */
  53. readonly eventCount?: number
  54. /** Physical artifact byte size, when the backend can provide it cheaply (JSONL); otherwise absent. */
  55. readonly sizeBytes?: number
  56. }
  57. /** Options for {@link SessionPersistence.create}. */
  58. export interface SessionPersistenceCreateOptions {
  59. /** Optional cancellation observed before backend work starts. */
  60. readonly signal?: AbortSignal
  61. /**
  62. * Exact fork-inherited prefix length. Required when `header.isSeeded` is
  63. * true and must be omitted (or `0`) otherwise; the backend refuses a
  64. * mismatch at create.
  65. */
  66. readonly inheritedEventCount?: SessionLogOffset
  67. }
  68. /**
  69. * Logical Session header paired with its exact inherited cut for body-bearing
  70. * storage operations. `isSeeded` marks fork lineage on the header; the
  71. * numeric cut travels beside it, never inside the replayable event log.
  72. */
  73. export interface SessionStorageMetadata {
  74. /** Validated immutable Session header. */
  75. readonly meta: SessionHeader
  76. /** Number of leading events inherited from the Session's fork parent. */
  77. readonly inheritedEventCount: SessionLogOffset
  78. }
  79. /** Immutable logical session read: storage metadata plus the complete validated event log. */
  80. export interface SessionInspection extends SessionStorageMetadata {
  81. /** Contiguous validated events from seq 0. */
  82. readonly events: readonly SessionEvent[]
  83. }
  84. /** Options for {@link SessionPersistence.open}. */
  85. export interface SessionPersistenceOpenOptions {
  86. /** Optional cancellation observed before backend work starts. */
  87. readonly signal?: AbortSignal
  88. }
  89. /** Options for {@link SessionPersistence.stat}. */
  90. export interface SessionPersistenceStatOptions {
  91. /** Optional cancellation for backend metadata reads. */
  92. readonly signal?: AbortSignal
  93. }
  94. /** Options for {@link SessionPersistence.list}. */
  95. export interface SessionPersistenceListOptions {
  96. /** Optional cancellation for backend listing work. */
  97. readonly signal?: AbortSignal
  98. }
  99. declare module '@deepseek-ai/cordis' {
  100. interface Context {
  101. sessionPersistence: SessionPersistence
  102. }
  103. }
  104. /**
  105. * Durable append-only session storage addressed through per-session handles.
  106. *
  107. * Storage semantics shared by every backend: events are contiguous from seq 0
  108. * and never rewritten; a torn physical tail is never returned to a reader and
  109. * is truncated by the write path before its first append; reads validate
  110. * current-format records only and refuse unknown vocabulary fail-closed.
  111. * `append` persists best-effort; `flush` — per handle or service-wide — is
  112. * the durability barrier.
  113. *
  114. * Visibility: a created session is observable through `stat`/`list`/`open`
  115. * in this process from the moment `create` resolves, even while a backend
  116. * defers physical materialization (a pure optimization); other processes see
  117. * the session only once it materializes, and a session that never
  118. * materialized before a crash never existed. `SessionHandle.flush` forces
  119. * materialization.
  120. *
  121. * Freshness: once an `append` or `flush` resolves, reads started afterwards
  122. * on this backend instance observe at least that prefix.
  123. */
  124. export abstract class SessionPersistence extends Service {
  125. constructor(ctx: Context) {
  126. super(ctx, 'sessionPersistence')
  127. }
  128. /**
  129. * Create a new stored session and take its write ownership.
  130. * @param header - the immutable header (id, version, cwd, lineage) to store.
  131. * @param options - optional cancellation.
  132. * @returns a `write` handle owned by the caller; close it to release ownership.
  133. * @throws {SessionAlreadyExistsError} when the id already exists.
  134. */
  135. abstract create(header: SessionHeader, options?: SessionPersistenceCreateOptions): Promise<SessionHandle>
  136. /**
  137. * Open an existing stored session.
  138. *
  139. * `read` never takes ownership and works while another handle (or process)
  140. * holds write ownership. `write` atomically claims single-writer ownership;
  141. * an existing active owner rejects.
  142. * @param id - the stored session to open.
  143. * @param access - `read` or `write`.
  144. * @param options - optional cancellation.
  145. * @returns the open handle.
  146. * @throws {SessionPersistenceNotFoundError} when the session does not exist.
  147. * @throws {SessionAlreadyOwnedError} for `write` when ownership is taken.
  148. */
  149. abstract open(id: SessionId, access: SessionAccess, options?: SessionPersistenceOpenOptions): Promise<SessionHandle>
  150. /**
  151. * Flush every active write handle owned by this service instance in one
  152. * durability barrier: each handle's routed live events drain durably and
  153. * its session materializes, exactly as that handle's own
  154. * `SessionHandle.flush` would. Read handles buffer nothing and are
  155. * untouched. A handle closed concurrently counts as flushed — close itself
  156. * drains durably.
  157. * @returns resolution once every write handle active at the call has flushed.
  158. * @throws {AggregateError} naming each session whose flush failed; the
  159. * remaining handles still flush.
  160. */
  161. abstract flush(): Promise<void>
  162. /**
  163. * Observe one stored session without reading its event log or taking
  164. * ownership.
  165. *
  166. * The snapshot's `revision` is an opaque change token comparable only
  167. * against revisions from the same service instance and session id: equal
  168. * revisions may be treated as an unchanged log; unequal revisions promise
  169. * nothing. Write-ownership churn does not change a revision. It exists for
  170. * derived read-model caches keyed off `stat`/`list`; it plays no part in
  171. * open, read, or resume.
  172. * @param id - the stored session to observe.
  173. * @param options - optional cancellation.
  174. * @returns the snapshot, or `undefined` when the session does not exist.
  175. */
  176. abstract stat(id: SessionId, options?: SessionPersistenceStatOptions): Promise<SessionPersistenceSnapshot | undefined>
  177. /**
  178. * List every stored session visible to this process, in no promised order.
  179. * @param options - optional cancellation.
  180. * @returns one snapshot per stored session.
  181. */
  182. abstract list(options?: SessionPersistenceListOptions): Promise<readonly SessionPersistenceSnapshot[]>
  183. }
  184. export default SessionPersistence