/** Shared Session history pagination, address validation and wire conversion. */ import { isAppendSurfaceEvent, SessionLogOffset, } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset as SessionLogOffsetType, SessionSeqCursor, } from '@deepseek-ai/dsh-session' import type { SessionObservation } from '@deepseek-ai/dsh-session-query' import type {} from '@deepseek-ai/dsh-subagent' import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import type { SessionAddress, SessionEventEntry, SessionHistoryRecord, SessionPageRequest, SessionWireEvent, } from './types.ts' /** Message budget applied when a history request omits `maxMessages`. */ export const DEFAULT_MAX_MESSAGES = 50 const MESSAGE_TYPES = new Set(['user/message', 'assistant/message']) /** * Reject an invalid message budget or turn window. * @param request - history window fields. */ export function validateHistoryWindow(request: Pick): void { if (request.maxMessages !== undefined && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) { throw new RemoteError('gateway/bad-request', 'maxMessages must be a positive safe integer', {}) } const window = request.turnWindow if (window !== undefined) { if (!Number.isSafeInteger(window.minMessages) || window.minMessages <= 0 || window.minMessages > (request.maxMessages ?? DEFAULT_MAX_MESSAGES)) { throw new RemoteError('gateway/bad-request', 'turnWindow.minMessages must be a positive safe integer no greater than maxMessages', {}) } if (!Number.isSafeInteger(window.minTurns) || window.minTurns <= 0) { throw new RemoteError('gateway/bad-request', 'turnWindow.minTurns must be a positive safe integer', {}) } } } /** * Select the Session whose log an address names. * @param address - ordinary Session or direct-subagent address. * @returns the Session whose log the address reads. */ export function addressId(address: SessionAddress): SessionId { return address.kind === 'session' ? address.sessionId : address.childSessionId } /** * Reject an address that does not match the observed Session. * @param address - ordinary Session or direct-subagent address. * @param header - observed Session header. * @param inheritedEventCount - observed fork-inherited event count. * @param projections - observed projections; subagent addresses require them. */ export function validateAddress( address: SessionAddress, header: SessionHeader, inheritedEventCount: SessionLogOffsetType, projections: SessionObservation['projections'], ): void { if (address.kind === 'session') { if (header.origin === 'subagent') { throw new RemoteError('session/agent-busy', 'subagent Sessions require their durable parent address', { reason: 'use subagent delivery for this child session', }) } return } if (header.origin !== 'subagent' || header.parentSession !== address.parentSessionId) { throw new RemoteError('subagent/unauthorized', 'subagent does not belong to the supplied parent', { childSessionId: address.childSessionId, }) } const identity = projections?.values.subagent if (identity === null) { throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is corrupt', { parentSessionId: address.parentSessionId, childSessionId: address.childSessionId, reason: 'corrupt', }) } if (identity === undefined || identity.seq < inheritedEventCount) { throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is unavailable', { parentSessionId: address.parentSessionId, childSessionId: address.childSessionId, reason: 'unsupported', }) } if (address.mode !== 'unknown' && identity.mode !== address.mode) { throw new RemoteError('subagent/unauthorized', 'subagent mode does not match the supplied address', { childSessionId: address.childSessionId, }) } } /** * Reject a missing Session using its address-specific error. * @param address - ordinary Session or direct-subagent address. * @returns never; this operation always throws. * @throws RemoteError `session/not-found` or `subagent/not-found`. */ export function rejectNotFound(address: SessionAddress): never { if (address.kind === 'session') { throw new RemoteError('session/not-found', `session "${address.sessionId}" not found`, { sessionId: address.sessionId }) } throw new RemoteError('subagent/not-found', 'subagent is unavailable', { parentSessionId: address.parentSessionId, childSessionId: address.childSessionId, }) } /** * Cut one message-aligned backwards page from a contiguous event prefix. * @param events - contiguous events starting at seq 0. * @param beforeSeq - exclusive upper offset of an older page. * @param maxMessages - message budget. * @param throughSeq - inclusive log cut. * @param turnWindow - optional turn-boundary stop rule. * @returns the page events and whether older events remain. */ export function paginate( events: readonly SessionEvent[], beforeSeq: SessionLogOffsetType | undefined, maxMessages: number, throughSeq: SessionSeqCursor, turnWindow?: SessionPageRequest['turnWindow'], ): { readonly events: SessionEvent[]; readonly hasMore: boolean } { const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1)) let count = 0 let turns = 0 let cut = SessionLogOffset(0) for (let index = end - 1; index >= 0; index--) { const event = events[index] as SessionEvent if (turnWindow !== undefined && event.type === 'turn/start') { turns++ if (count >= turnWindow.minMessages && turns >= turnWindow.minTurns) { cut = SessionLogOffset(index) break } } if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event)) continue count++ const sources = event.sourceEventSeqs let groupStart = event.seq if (sources !== undefined) { for (const source of sources) { if (source < groupStart) groupStart = source } } if (count >= maxMessages) { cut = SessionLogOffset(groupStart) break } } return { events: events.slice(cut, end), hasMore: cut > 0 } } /** * Convert a committed event to its wire entry. * @param event - committed Session event. * @returns the wire event entry. */ export function entryFor(event: SessionEvent): SessionEventEntry { return { type: 'event', // Session.append validates and freezes event data as JSON before publication. event: event as unknown as SessionWireEvent, } } /** * Encode one bounded logical page without changing its pagination cut. * @param events - page events. * @returns the wire records. */ export function pageRecords(events: readonly SessionEvent[]): SessionHistoryRecord[] { return events.map(entryFor) }