fix(session-query): address review round 1

This commit is contained in:
Hypatia May
2026-07-10 17:29:52 +08:00
parent aa1dc0e2c7
commit 18028cad4f
14 changed files with 386 additions and 18 deletions

View File

@@ -18,6 +18,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [llm-streaming.md](llm-streaming.md) | the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam |
| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, the turn-enclosure invariant |
| [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` |
| [session-query.md](session-query.md) | the retrieval seam: logical session/event records, filters, traces, search pages, extractors, and provider synchronization types |
| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, the `tools/pre-execute`/`tools/post-execute` pipeline |
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashTask`s |

View File

@@ -73,6 +73,20 @@ interface CreateSessionOptions {
Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
## `SessionPersistedChange` — committed-log notification range
The observe-only `session/persisted` event carries the canonical header and the committed range. A repair can report `toSeq < fromSeq` when it only removes a torn fragment.
Source: [`packages/session-persistence/session-persistence/src/index.ts`](../../packages/session-persistence/session-persistence/src/index.ts)
```ts type-equiv
export interface SessionPersistedChange {
kind: 'append' | 'repair'
fromSeq: number
toSeq: number
}
```
## The backends
Both implement the same abstract `SessionPersistence` (create/append/load/list over `SessionEvent`) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic:

View File

@@ -0,0 +1,221 @@
# Session Query
The provider-neutral retrieval seam over live and optionally persisted sessions. The [package contract](../../packages/session-query/session-query) owns resolution, lifecycle, synchronization, and error behavior; this page catalogs the public data exchanged by callers, extractors, and search providers.
Source: [`packages/session-query/session-query/src/types.ts`](../../packages/session-query/session-query/src/types.ts)
## Logical records and filters
`SessionRecord` exposes source availability independently from its live-preferred header. `SessionEventRecord` classifies every raw event against the folded surface.
```ts type-equiv
export type SessionEventSurface = 'current' | 'shadowed' | 'log-only'
```
```ts type-equiv
export interface SessionRecord {
header: SessionHeader
live: boolean
persisted: boolean
}
```
```ts type-equiv
export interface SessionEventRecord {
sessionId: SessionId
seq: number
type: SessionEventType
time: number
surface: SessionEventSurface
}
```
Filters are serializable discriminated specs. Each spec is one transform in a chain; the literal types below are shared by in-memory filtering and provider pre-ranking requests.
```ts type-equiv
export interface SessionQueryRange {
from?: number
to?: number
}
```
```ts type-equiv
export type SessionResultFilter =
| { kind: 'id'; values: readonly SessionId[] }
| { kind: 'cwd'; values: readonly (string | null)[] }
| { kind: 'created-at'; range: SessionQueryRange }
| { kind: 'parent'; values: readonly (SessionId | null)[] }
| { kind: 'availability'; values: readonly ('live' | 'persisted')[] }
```
```ts type-equiv
export type SessionEventResultFilter =
| { kind: 'seq'; range: SessionQueryRange }
| { kind: 'time'; range: SessionQueryRange }
| { kind: 'type'; values: readonly SessionEventType[] }
| { kind: 'surface'; values: readonly SessionEventSurface[] }
```
## Search requests and pages
Both scopes use the same opaque-cursor page envelope. Session hits carry exactly one best event; event hits add only a plain-text snippet to the lightweight record.
```ts type-equiv
export interface SessionQueryExecContext {
readonly signal?: AbortSignal
}
```
```ts type-equiv
export type SessionSearchProviderStatus =
| { readonly available: true }
| { readonly available: false; readonly reason: 'misconfigured' | 'unavailable' }
```
```ts type-equiv
export interface SessionSearchPageRequest {
limit?: number
cursor?: string
}
```
```ts type-equiv
export interface SessionSearchRequest extends SessionSearchPageRequest {
query: string
sessionFilters?: readonly SessionResultFilter[]
eventFilters?: readonly SessionEventResultFilter[]
}
```
```ts type-equiv
export interface SessionEventSearchRequest extends SessionSearchPageRequest {
sessionId: SessionId
query: string
filters?: readonly SessionEventResultFilter[]
}
```
```ts type-equiv
export interface SessionEventSearchHit extends SessionEventRecord {
snippet: string
}
```
```ts type-equiv
export interface SessionSearchHit extends SessionRecord {
bestMatch: SessionEventSearchHit
}
```
```ts type-equiv
export interface SessionSearchPage<T> {
providerId: string
items: readonly T[]
nextCursor?: string
}
```
## Event reads and traces
An event read returns the full target plus a bounded raw-log window. Trace records retain lightweight seq links so callers choose which related event bodies to read.
```ts type-equiv
export interface SessionEventReadRequest {
sessionId: SessionId
seq: number
before?: number
after?: number
}
```
```ts type-equiv
export interface SessionEventWindow {
session: SessionRecord
target: SessionEvent
events: SessionEvent[]
startSeq: number
endSeq: number
}
```
```ts type-equiv
export interface SessionLineageNode {
session: SessionRecord
children: SessionLineageNode[]
}
```
```ts type-equiv
export interface SessionLineageTrace {
target: SessionRecord
parents: SessionRecord[]
root?: SessionRecord
unresolvedParentId?: SessionId
children: SessionLineageNode[]
}
```
```ts type-equiv
export interface SessionEventTrace {
target: SessionEventRecord
shadowedBy?: number
replacementChain: number[]
shadows: number[]
references: number[]
referencedBy: number[]
}
```
## Extraction and provider synchronization
Custom extractors are keyed by declaration-merged event or content discriminants and carry stable cache-invalidation versions. Providers receive complete event documents grouped into independently replaceable persisted and live snapshots.
```ts type-equiv
export interface SessionEventTextExtractor<K extends SessionEventType = SessionEventType> {
version: string
extract(event: SessionEvent<K>): readonly string[]
}
```
```ts type-equiv
export interface SessionContentTextExtractor<K extends ContentBlockType = ContentBlockType> {
version: string
extract(block: ContentBlockMap[K]): readonly string[]
}
```
```ts type-equiv
export interface SessionIndexDocument extends SessionEventRecord {
text: string
}
```
```ts type-equiv
export interface SessionIndexSnapshot {
session: SessionRecord
fingerprint: string
documents: readonly SessionIndexDocument[]
}
```
```ts type-equiv
export interface SessionPersistedIndexEntry {
sessionId: SessionId
fingerprint: string
}
```
```ts type-equiv
export interface SessionSearchProvider {
readonly id: string
status(): SessionSearchProviderStatus
persistedInventory(): Promise<readonly SessionPersistedIndexEntry[]>
setPersistedActive(active: boolean): Promise<void>
replacePersisted(snapshot: SessionIndexSnapshot): Promise<void>
removePersisted(sessionId: SessionId): Promise<void>
replaceLive(snapshot: SessionIndexSnapshot): Promise<void>
removeLive(sessionId: SessionId): Promise<void>
searchSessions(request: SessionSearchRequest, exec?: SessionQueryExecContext): Promise<SessionSearchPage<SessionSearchHit>>
searchEvents(request: SessionEventSearchRequest, exec?: SessionQueryExecContext): Promise<SessionSearchPage<SessionEventSearchHit>>
}
```

View File

@@ -196,6 +196,26 @@ export interface SurfaceNode {
}
```
### `SurfaceFoldReplacement` and `SurfaceFoldResult` — a complete surface replay
`foldSurface(events)` returns detached current nodes together with the actual node seqs shadowed by each declared replacement range. `SurfaceManager` uses the same transition functions for its incremental cache.
```ts type-equiv
export interface SurfaceFoldReplacement {
seq: number
start: number
end: number
shadowedSeqs: number[]
}
```
```ts type-equiv
export interface SurfaceFoldResult {
nodes: SurfaceNode[]
replacements: SurfaceFoldReplacement[]
}
```
## Derived history: `deriveMessages()` and `deriveEventMessage()`
`Session.deriveMessages()` projects the event log into the `Message[]` the model sees — cached (each surface node projected once, when first seen; a surface rewrite rebuilds) and frozen (a fresh array per call over shared, deep-frozen messages, so mutating logged history through a projection is unrepresentable). `deriveEventMessage(event)` is the per-node pure function the fold applies — public so external reconstructors and the dev invariant project a log prefix with exactly the same rules and cannot disagree with the cache. The projection rules: