fix: serialize paginated session searches

This commit is contained in:
Hypatia May
2026-07-24 19:47:23 +08:00
parent 795af3174e
commit 66585635c8
16 changed files with 133 additions and 39 deletions

View File

@@ -28,6 +28,7 @@ The database is disposable but reset is guarded: every recognized schema version
| `maxLimit` | `100` | Largest accepted request page size; at most `Number.MAX_SAFE_INTEGER - 1`. |
| `snippetChars` | `240` | Maximum snippet length in Unicode code points. |
| `readWindowMax` | `50` | Maximum `before` or `after` raw-event count for inherited `readEvent()`. |
| `persistedInspectConcurrency` | `4` | Maximum concurrent persisted-log inspections for inherited batch reads; must be a positive safe integer. |
## Tokenizer and limits

View File

@@ -15,6 +15,7 @@ import type {
SessionPersistenceSnapshot,
} from '@deepseek-ai/dsh-session-persistence'
import SessionQueryService, {
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
SESSION_QUERY_READ_WINDOW_MAX,
SessionQueryError,
SessionSearchCursor,
@@ -87,6 +88,8 @@ export interface Config extends SessionQueryConfig {
maxLimit?: number
/** Maximum snippet length in Unicode code points. Defaults to 240. */
snippetChars?: number
/** Maximum concurrent persisted-log inspections in one inherited batch read. Defaults to 4. */
persistedInspectConcurrency?: number
}
interface ResolvedConfig {
@@ -96,6 +99,7 @@ interface ResolvedConfig {
maxLimit: number
snippetChars: number
readWindowMax: number
persistedInspectConcurrency: number
}
interface ObservedSession {
@@ -176,6 +180,11 @@ export class SessionQuerySqlite extends SessionQueryService {
maxLimit: z.number().step(1).min(1).max(SQLITE_MAX_PAGE_LIMIT).default(SESSION_QUERY_SQLITE_MAX_LIMIT),
snippetChars: z.number().step(1).min(1).default(SESSION_QUERY_SQLITE_SNIPPET_CHARS),
readWindowMax: z.number().step(1).min(0).default(SESSION_QUERY_READ_WINDOW_MAX),
persistedInspectConcurrency: z.number()
.step(1)
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY),
})
/** Validated and defaulted backend configuration. */
@@ -937,6 +946,8 @@ function resolveConfig(config: Config): ResolvedConfig {
maxLimit: config.maxLimit ?? SESSION_QUERY_SQLITE_MAX_LIMIT,
snippetChars: config.snippetChars ?? SESSION_QUERY_SQLITE_SNIPPET_CHARS,
readWindowMax: config.readWindowMax ?? SESSION_QUERY_READ_WINDOW_MAX,
persistedInspectConcurrency: config.persistedInspectConcurrency
?? SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
}
if (typeof resolved.path !== 'string' || resolved.path.trim().length === 0) {
throw invalidConfig('path must not be blank')
@@ -947,6 +958,12 @@ function resolveConfig(config: Config): ResolvedConfig {
if (!Number.isInteger(resolved.readWindowMax) || resolved.readWindowMax < 0) {
throw invalidConfig('readWindowMax must be a non-negative integer')
}
if (
!Number.isSafeInteger(resolved.persistedInspectConcurrency)
|| resolved.persistedInspectConcurrency < 1
) {
throw invalidConfig('persistedInspectConcurrency must be a positive safe integer')
}
if (resolved.defaultLimit > resolved.maxLimit) {
throw invalidConfig('defaultLimit must be less than or equal to maxLimit')
}

View File

@@ -13,6 +13,7 @@ import SessionQuerySqlite, {
SESSION_QUERY_SQLITE_SCHEMA_VERSION,
} from '@deepseek-ai/dsh-session-query-sqlite'
import {
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
SessionQueryError,
SessionSearchCursor,
type SessionAvailability,
@@ -167,6 +168,29 @@ async function liveContext(config: ConstructorParameters<typeof SessionQuerySqli
}
describe('SQLite session search', () => {
it('defaults and validates persisted inspection concurrency through its Cordis config', async () => {
const defaultCtx = await liveContext()
expect((defaultCtx.sessionQuery as SessionQuerySqlite).config.persistedInspectConcurrency)
.toBe(SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY)
const configuredValue = 2
const configured = new SessionQuerySqlite.Config({
path: ':memory:',
persistedInspectConcurrency: configuredValue,
})
expect(configured.persistedInspectConcurrency).toBe(configuredValue)
const configuredCtx = await liveContext(configured)
expect((configuredCtx.sessionQuery as SessionQuerySqlite).config.persistedInspectConcurrency)
.toBe(configuredValue)
for (const persistedInspectConcurrency of [0, Number.MAX_SAFE_INTEGER + 1]) {
expect(() => new SessionQuerySqlite.Config({
path: ':memory:',
persistedInspectConcurrency,
})).toThrow()
}
})
it('searches two-character Unicode61 tokens in live-only sessions', async () => {
const ctx = await liveContext({ path: ':memory:', snippetChars: 20 })
const session = ctx.sessions.create(SessionId('live'), {
@@ -486,6 +510,8 @@ describe('SQLite session search', () => {
{ path: ':memory:', maxLimit: 1e100 },
{ path: ':memory:', snippetChars: 0 },
{ path: ':memory:', readWindowMax: -1 },
{ path: ':memory:', persistedInspectConcurrency: 0 },
{ path: ':memory:', persistedInspectConcurrency: Number.MAX_SAFE_INTEGER + 1 },
{ path: ':memory:', defaultLimit: 3, maxLimit: 2 },
{ path: ':memory:', journalMode: 'memory' },
]) {