mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
/**
|
|
* Opt-in SQLite persistence provider. Logical sessions remain unchanged;
|
|
* the physical backend packs eligible chunk runs into schema-18 rows.
|
|
* @module @deepseek-ai/dsh-session-persistence-sqlite
|
|
*/
|
|
|
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
import z from '@deepseek-ai/schemastery'
|
|
import type {
|
|
Session,
|
|
SessionEvent,
|
|
SessionHeader,
|
|
SessionId,
|
|
SessionPreparation,
|
|
} from '@deepseek-ai/dsh-session'
|
|
import {
|
|
DEFAULT_PREPARED_SESSION_CACHE_SIZE,
|
|
DEFAULT_WRITE_BATCH_MAX_DELAY_MS,
|
|
MAX_WRITE_BATCH_DELAY_MS,
|
|
type BorrowedSessionSource,
|
|
PersistenceCoordinator,
|
|
SessionPersistence,
|
|
type SessionInspection,
|
|
type SessionLocation,
|
|
type SessionPersistenceSnapshot,
|
|
} from '@deepseek-ai/dsh-session-persistence'
|
|
import type { JournalMode } from './schema.ts'
|
|
import { SqliteStore } from './store.ts'
|
|
|
|
export { SCHEMA_VERSION } from './schema.ts'
|
|
|
|
/** Default wait for another SQLite connection's write reservation. */
|
|
export const DEFAULT_BUSY_TIMEOUT_MS = 5_000
|
|
/** Largest busy timeout accepted by SQLite's signed millisecond interface. */
|
|
export const MAX_BUSY_TIMEOUT_MS = 2_147_483_647
|
|
|
|
/** Plugin configuration. */
|
|
export interface Config {
|
|
/** SQLite database path, or `:memory:` for an in-process database. */
|
|
path: string
|
|
/** Durable SQLite journal mode; defaults to `wal`. */
|
|
journalMode?: JournalMode
|
|
/** Maximum wait for another SQLite connection's lock; defaults to 5,000 ms. */
|
|
busyTimeoutMs?: number
|
|
/** Maximum cold Session preparations retained for history-to-resume reuse. */
|
|
preparedSessionCacheSize?: number
|
|
/** Fixed live-event coalescing window; not a backend completion deadline. */
|
|
writeBatchMaxDelayMs?: number
|
|
}
|
|
|
|
/**
|
|
* SQLite `SessionPersistence` provider with a schema-owned physical codec.
|
|
*/
|
|
export class SqliteSessionPersistence extends SessionPersistence {
|
|
override readonly supportsRawArtifacts = false
|
|
override readonly name = 'session-persistence-sqlite'
|
|
|
|
static inject = ['sessions']
|
|
|
|
static Config: z<Config> = z.object({
|
|
path: z.string().required(),
|
|
journalMode: z.union(['wal', 'delete', 'truncate', 'persist'] as const).default('wal'),
|
|
busyTimeoutMs: z.number().step(1).min(0).max(MAX_BUSY_TIMEOUT_MS).default(DEFAULT_BUSY_TIMEOUT_MS),
|
|
preparedSessionCacheSize: z.number().step(1).min(1).default(DEFAULT_PREPARED_SESSION_CACHE_SIZE),
|
|
writeBatchMaxDelayMs: z.number().step(1).min(1).max(MAX_WRITE_BATCH_DELAY_MS)
|
|
.default(DEFAULT_WRITE_BATCH_MAX_DELAY_MS),
|
|
})
|
|
|
|
private readonly store: SqliteStore
|
|
private readonly coordinator: PersistenceCoordinator<number>
|
|
|
|
constructor(ctx: Context, public config: Config) {
|
|
super(ctx)
|
|
const preparedSessionCacheSize = config.preparedSessionCacheSize
|
|
?? DEFAULT_PREPARED_SESSION_CACHE_SIZE
|
|
const writeBatchMaxDelayMs = config.writeBatchMaxDelayMs
|
|
?? DEFAULT_WRITE_BATCH_MAX_DELAY_MS
|
|
this.store = new SqliteStore({
|
|
path: config.path,
|
|
journalMode: config.journalMode ?? 'wal',
|
|
busyTimeoutMs: config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS,
|
|
})
|
|
this.coordinator = new PersistenceCoordinator(this.ctx, this.store, {
|
|
preparedSessionCacheSize,
|
|
writeBatchMaxDelayMs,
|
|
})
|
|
}
|
|
|
|
/** Reject self-contained path and ownership failures without loading Node SQLite. */
|
|
protected async [Service.init](): Promise<void> {
|
|
await this.store.validatePath()
|
|
}
|
|
|
|
/** SQLite has one database, not an independent per-session artifact. */
|
|
locate(_meta: SessionHeader): SessionLocation | undefined {
|
|
return undefined
|
|
}
|
|
|
|
create(meta: SessionHeader): Promise<void> {
|
|
return this.coordinator.create(meta)
|
|
}
|
|
|
|
override ensureMaterialized(session: Session): Promise<void> {
|
|
return this.coordinator.ensureMaterialized(session)
|
|
}
|
|
|
|
append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
|
|
return this.coordinator.append(id, events)
|
|
}
|
|
|
|
override prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation> {
|
|
return this.coordinator.prepare(id, signal)
|
|
}
|
|
|
|
load(id: SessionId): Promise<SessionInspection> {
|
|
return this.coordinator.load(id)
|
|
}
|
|
|
|
inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection> {
|
|
return this.coordinator.inspect(id, signal)
|
|
}
|
|
|
|
override borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSessionSource> {
|
|
return this.coordinator.borrowSession(id, signal)
|
|
}
|
|
|
|
readFrom(
|
|
id: SessionId,
|
|
fromSeq: number,
|
|
signal?: AbortSignal,
|
|
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
|
|
return this.coordinator.readFrom(id, fromSeq, signal)
|
|
}
|
|
|
|
list(signal?: AbortSignal): Promise<SessionHeader[]> {
|
|
return this.store.list(signal)
|
|
}
|
|
|
|
listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]> {
|
|
return this.store.listSnapshots(signal)
|
|
}
|
|
}
|
|
|
|
export default SqliteSessionPersistence
|