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

@@ -15,7 +15,7 @@
- `traceSession(sessionId)` reads the corpus once and returns immediate-to-outward ancestors plus deterministic recursive descendant trees. `complete: false` identifies the first missing parent; a target-connected cycle fails with `SESSION_QUERY_INVALID_LINEAGE`.
- `traceEvent(request)` loads the logical log once and returns its cloned source header with direct positional replacements and direct logged provenance. `replacementChain` follows positional replacers to the final replacement; provenance links remain non-transitive.
Persistence is optional and may mount or unmount dynamically. Cross-corpus listing and lineage tracing fail with `SESSION_QUERY_PERSISTENCE_FAILED` while mounted persistence is unreadable. A title, event read, or trace targeting a known live session does not consult persistence, so durable backend health cannot make current in-memory state unreadable. Persisted title and event operations list before loading and reject a metadata mismatch rather than combining inconsistent observations. A batch title observation performs one metadata listing, inspects its unique persisted ids with at most four workers, and preserves each title's own observed header for downstream authorization. Cancellation starts no queued inspections and rejects only after already-started workers settle. `listSessions()` remains lightweight and does not load logs or index titles.
Persistence is optional and may mount or unmount dynamically. Cross-corpus listing and lineage tracing fail with `SESSION_QUERY_PERSISTENCE_FAILED` while mounted persistence is unreadable. A title, event read, or trace targeting a known live session does not consult persistence, so durable backend health cannot make current in-memory state unreadable. Persisted title and event operations list before loading and reject a metadata mismatch rather than combining inconsistent observations. A batch title observation performs one metadata listing, inspects its unique persisted ids with at most `persistedInspectConcurrency` workers, and preserves each title's own observed header for downstream authorization. Cancellation starts no queued inspections and rejects only after already-started workers settle. `listSessions()` remains lightweight and does not load logs or index titles.
## Filtering and extraction
@@ -38,6 +38,7 @@ The package has no provider coordinator, fallback implementation, or standalone
| Key | Default | Contract |
|---|---:|---|
| `readWindowMax` | `50` | Maximum `before` or `after` raw-event count. |
| `persistedInspectConcurrency` | `4` | Maximum concurrent persisted-log inspections in one batch read; must be a positive safe integer. |
## Model Experience

View File

@@ -5,10 +5,15 @@ import { HarnessError } from '@deepseek-ai/dsh-llm'
/** Default maximum `before`/`after` raw-event window. */
export const SESSION_QUERY_READ_WINDOW_MAX = 50
/** Default maximum number of concurrent persisted-log inspections in one batch read. */
export const SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY = 4
/** Backend-independent configuration inherited by every session-query implementation. */
export interface Config {
/** Maximum accepted raw read context on either side. Defaults to 50. */
readWindowMax?: number
/** Maximum concurrent persisted-log inspections in one batch read. Defaults to 4. */
persistedInspectConcurrency?: number
}
/** Stable machine-routable failure taxonomy for session reads, traces, and search. */

View File

@@ -28,15 +28,15 @@ export type LogicalProjectionResult<Value> =
| { sessionId: SessionId; status: 'fulfilled'; value: Value }
| { sessionId: SessionId; status: 'rejected'; reason: unknown }
/** Bound persisted observation fan-out for public batch title reads. */
const PERSISTED_INSPECT_CONCURRENCY = 4
/** Resolves a live-preferred corpus against the persistence service mounted now. */
export class SessionCorpus {
private _persistence: SessionPersistence | undefined
private readonly _optionalPersistenceFiber: Fiber
constructor(private readonly _ctx: Context) {
constructor(
private readonly _ctx: Context,
private readonly _persistedInspectConcurrency: number,
) {
this._optionalPersistenceFiber = _ctx.inject(['sessionPersistence'], (childCtx: Context) => {
const service = childCtx.sessionPersistence
this._persistence = service
@@ -188,7 +188,7 @@ export class SessionCorpus {
await resolvePersisted(unresolved[index] as SessionId)
}
}
const workerCount = Math.min(PERSISTED_INSPECT_CONCURRENCY, unresolved.length)
const workerCount = Math.min(this._persistedInspectConcurrency, unresolved.length)
const settlements = await Promise.allSettled(
Array.from({ length: workerCount }, () => worker()),
)

View File

@@ -31,6 +31,7 @@ import type {
SessionTitleObservationResult,
} from './types.ts'
import {
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
SESSION_QUERY_READ_WINDOW_MAX,
SessionQueryError,
type Config,
@@ -48,7 +49,11 @@ import * as tracing from './tracing.ts'
export type * from './types.ts'
export { SessionSearchCursor } from './cursor.ts'
export type { Config, SessionQueryErrorCode } from './config.ts'
export { SESSION_QUERY_READ_WINDOW_MAX, SessionQueryError } from './config.ts'
export {
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
SESSION_QUERY_READ_WINDOW_MAX,
SessionQueryError,
} from './config.ts'
export { extractSessionEventText } from './extraction.ts'
export { buildSessionEventRecords, buildSessionEventSearchDocuments } from './documents.ts'
export {
@@ -88,7 +93,15 @@ export abstract class SessionQueryService extends Service {
'SESSION_QUERY_INVALID_CONFIG',
)
}
this._corpus = new SessionCorpus(ctx)
const persistedInspectConcurrency = config.persistedInspectConcurrency
?? SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY
if (!Number.isSafeInteger(persistedInspectConcurrency) || persistedInspectConcurrency < 1) {
throw new SessionQueryError(
'session-query: persistedInspectConcurrency must be a positive safe integer',
'SESSION_QUERY_INVALID_CONFIG',
)
}
this._corpus = new SessionCorpus(ctx, persistedInspectConcurrency)
}
/**

View File

@@ -4,6 +4,7 @@ import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/ds
import type { SessionEvent, SessionHeader, SessionId as SessionIdType } from '@deepseek-ai/dsh-session'
import SessionPersistence, { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence'
import SessionQueryService, {
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
type SessionEventSurface,
type SessionQueryErrorCode,
} from '@deepseek-ai/dsh-session-query'
@@ -372,7 +373,7 @@ describe('session-query exact reads', () => {
const results = await ctx.sessionQuery.readTitleSnapshots(entries.map(entry => entry.meta.id))
expect(maximum).toBe(4)
expect(maximum).toBe(SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY)
expect(TestPersistence.listCalls).toBe(1)
expect(TestPersistence.inspectCalls).toEqual(entries.map(entry => entry.meta.id))
expect(results.map(result => result.sessionId)).toEqual(entries.map(entry => entry.meta.id))
@@ -466,7 +467,8 @@ describe('session-query exact reads', () => {
events: eventLog(`queued-${index}`),
}))
TestPersistence.reset(entries)
const ctx = await liveContext()
const persistedInspectConcurrency = 2
const ctx = await liveContext({ persistedInspectConcurrency })
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('cancel queued title batch')
@@ -490,17 +492,21 @@ describe('session-query exact reads', () => {
() => { batchSettled = true },
() => { batchSettled = true },
)
await vi.waitFor(() => { expect(TestPersistence.inspectCalls).toHaveLength(4) })
await vi.waitFor(() => {
expect(TestPersistence.inspectCalls).toHaveLength(persistedInspectConcurrency)
})
controller.abort(reason)
await vi.waitFor(() => { expect(abortsObserved).toBe(4) })
await vi.waitFor(() => { expect(abortsObserved).toBe(persistedInspectConcurrency) })
expect(batchSettled).toBe(false)
expect(TestPersistence.inspectCalls).toEqual(entries.slice(0, 4).map(entry => entry.meta.id))
expect(TestPersistence.inspectCalls)
.toEqual(entries.slice(0, persistedInspectConcurrency).map(entry => entry.meta.id))
for (const release of releases) release()
await expect(pending).rejects.toBe(reason)
expect(inspectionsSettled).toBe(4)
expect(TestPersistence.inspectCalls).toEqual(entries.slice(0, 4).map(entry => entry.meta.id))
expect(inspectionsSettled).toBe(persistedInspectConcurrency)
expect(TestPersistence.inspectCalls)
.toEqual(entries.slice(0, persistedInspectConcurrency).map(entry => entry.meta.id))
})
it('passes cancellation into a stalled persisted title listing and rejects with its reason', async () => {
@@ -907,10 +913,16 @@ describe('session-query exact reads', () => {
const direct = new Context()
await direct.plugin(SessionStore)
expect(new TestSessionQueryService(direct)).toBeInstanceOf(SessionQueryService)
const invalid = new Context()
await invalid.plugin(SessionStore)
expect(() => new TestSessionQueryService(invalid, { readWindowMax: -1 }))
.toThrow(expectCode('SESSION_QUERY_INVALID_CONFIG'))
for (const config of [
{ readWindowMax: -1 },
{ persistedInspectConcurrency: 0 },
{ persistedInspectConcurrency: Number.MAX_SAFE_INTEGER + 1 },
]) {
const invalid = new Context()
await invalid.plugin(SessionStore)
expect(() => new TestSessionQueryService(invalid, config))
.toThrow(expectCode('SESSION_QUERY_INVALID_CONFIG'))
}
})
it('leaves the optional persistence dependency optional', async () => {