The persistence seam is now create/open/stat/list returning per-session SessionHandles (read/append/flush/close); every log read and write flows through the owning handle. The seam package exports only the service and handle contracts, consumer-visible errors, and pure durable-data validation helpers; each backend owns its complete storage runtime, and the shared contract suites pin equivalent observable behavior. The backend routes published sessions' live events by id into the active write handle; agent-loop only acquires, seeds, and closes the handle. Resume appends interruptedTurnClosers through its write handle; session-query owns the revision-keyed cold cache. Legacy-only surfaces are removed in the same swap: locate/readRaw/supportsRawArtifacts, the legacy event-shape read migration, zstd torn-frame salvage, DSH_SESSION_JSONL, and hook transcript_path population; a torn final zstd frame is discarded whole; the session-list cold blank probe returns on stat metadata (eventCount derived from the last physical row, sizeBytes). The WebUI ZIP export serializes the logical log from a read handle, so both backends export identically. Refs #3245
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
/** Minimal concrete Session query for Agent Team continuation tests. */
|
|
|
|
import { SessionLogOffset } from '@deepseek-ai/dsh-session'
|
|
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
|
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
|
|
import type { SessionObservation, SessionObservationOptions } from '@deepseek-ai/dsh-session-query'
|
|
|
|
/** Undisposable immutable cut over one session's header and events. */
|
|
function cut(
|
|
source: 'live' | 'prepared',
|
|
header: SessionHeader,
|
|
events: readonly SessionEvent[],
|
|
): SessionObservation {
|
|
const lease = (): SessionObservation => ({
|
|
source,
|
|
header,
|
|
inheritedEventCount: SessionLogOffset(0),
|
|
events,
|
|
cursor: events.at(-1)?.seq ?? -1,
|
|
retain: lease,
|
|
[Symbol.dispose]: () => {},
|
|
})
|
|
return lease()
|
|
}
|
|
|
|
/** Session query implementation whose search faces are outside these tests. */
|
|
export class TestSessionQuery extends SessionQueryEngine {
|
|
static override inject = ['sessions', 'sessionPersistence']
|
|
|
|
/** Live-preferred observation backed directly by a short-lived persistence read handle. */
|
|
override async observeSession(
|
|
sessionId: SessionId,
|
|
options: SessionObservationOptions = {},
|
|
): Promise<SessionObservation> {
|
|
const live = this.ctx.sessions.get(sessionId)
|
|
if (live !== undefined) return cut('live', live.header, live.snapshotEvents())
|
|
const handle = await this.ctx.sessionPersistence.open(
|
|
sessionId,
|
|
'read',
|
|
options.signal === undefined ? {} : { signal: options.signal },
|
|
)
|
|
try {
|
|
return cut(
|
|
'prepared',
|
|
handle.header,
|
|
await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal }),
|
|
)
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|
|
|
|
override searchSessions(): Promise<never> {
|
|
return Promise.reject(new Error('session search is not configured in this test'))
|
|
}
|
|
|
|
override searchEvents(): Promise<never> {
|
|
return Promise.reject(new Error('event search is not configured in this test'))
|
|
}
|
|
}
|