Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md
#	.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.md
#	.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml
#	docs/architecture.i18n.yaml
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/session.i18n.yaml
#	docs/core-data-structures/tools.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode/session.jsonl
#	examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl
#	packages/context/session-reference/README.md
#	packages/core/agent-loop/tests/agent.spec.ts
#	packages/hooks/hooks-claude/tests/coverage-cases.ts
#	packages/host/runtime/tests/host-runtime.spec.ts
#	packages/llm/llm-retry/tests/retry.spec.ts
#	packages/session-persistence/session-persistence/src/coordinator.ts
#	packages/support/acp-snapshot/README.md
#	packages/support/acp-snapshot/src/normalize.ts
#	packages/ui/acp/acp-feature-support.md
#	packages/ui/acp/src/codec.ts
#	packages/ui/acp/src/index.ts
#	packages/ui/acp/tests/bridge.spec.ts
#	packages/ui/acp/tests/codec.spec.ts
#	packages/ui/acp/tests/config-options.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/ui/acp/tests/stream-update.spec.ts
#	packages/ui/acp/tests/turns.spec.ts
This commit is contained in:
_Kerman
2026-07-26 14:05:33 +08:00
1062 changed files with 33621 additions and 30750 deletions

View File

@@ -6,5 +6,6 @@ Trusted exact reads, relationship traces, provider-independent semantic filterin
|---|---|---|
| [`session-query/`](session-query/README.md) | Combined service contract with concrete logical-corpus reads, traces, and semantic filters plus abstract full-text methods | `ctx.sessionQuery` |
| [`session-query-sqlite/`](session-query-sqlite/README.md) | Concrete service backend with SQLite FTS5 persistent bases and live overlays | `ctx.sessionQuery` |
| [`tool-session-query/`](tool-session-query/README.md) | Workspace-authorized model-facing search, lineage, relationship, and exact event tools | — |
The family is independent of compaction: it reads canonical lineage, surface operations, logged provenance, and semantic event text but does not participate in compaction policy or execution. One abstract service combines every query operation, and one concrete backend owns the full-text lifecycle without a provider registry or coordinator.
The query service is independent of compaction: it reads canonical lineage, surface operations, logged provenance, and semantic event text but does not participate in compaction policy or execution. One abstract service combines every query operation, one concrete backend owns the full-text lifecycle without a provider registry or coordinator, and the consumer leaves oversized plain-text results to the generic post-execute spill policy.

View File

@@ -28,12 +28,13 @@ 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
The index uses FTS5 `unicode61`. In the implementation experiment it supported the two-character query `AI` and produced an index about 2.1× smaller than the trigram alternative. The trade-off is token/phrase recall rather than arbitrary substring recall: `AI` does not match the token `BRAID`. Use `ctx.sessionQuery.filterEvents()` with a `text` clause when a literal whitespace-flexible substring scan is required. NUL is rejected in queries; reserved highlight markers and NUL in documents are normalized before indexing so presentation markers cannot collide with source text.
Abort signals stop queued work and caller waits around asynchronous source observation. Node's synchronous `DatabaseSync` API cannot interrupt a MATCH statement already executing on the JavaScript thread; the signal is checked immediately before and after the serialized observation/reconciliation boundary.
Abort signals stop queued work and flow unchanged through snapshot listing and non-mutating inspection. Once source work starts, the serialized state machine awaits that backend promise itself—even when a backend ignores cancellation—then checks the signal before starting any further listing, inspection, reconciliation, or query work. The caller therefore observes cancellation only after started backend work is quiescent, and a later search cannot enter the serializer while that cleanup is pending. Node's synchronous `DatabaseSync` API cannot interrupt a metadata or MATCH statement already executing on the JavaScript thread; signals are checked immediately before and after those non-preemptible calls.
## Model Experience

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,
@@ -25,6 +26,7 @@ import type {
Config as SessionQueryConfig,
SessionEventSearchDocument,
SessionEventSearchHit,
SessionEventSearchPage,
SessionEventSearchRequest,
SessionSearchExecContext,
SessionSearchHit,
@@ -86,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 {
@@ -95,6 +99,7 @@ interface ResolvedConfig {
maxLimit: number
snippetChars: number
readWindowMax: number
persistedInspectConcurrency: number
}
interface ObservedSession {
@@ -133,7 +138,7 @@ interface IndexedLiveRow {
generation: number
}
interface SearchRow {
interface SessionHeaderRow {
session_id: string
version: number
created_at: number
@@ -141,6 +146,9 @@ interface SearchRow {
parent_session: string | null
seed_length: number | null
delegation_depth: number | null
}
interface SearchRow extends SessionHeaderRow {
live: number
persisted: number
seq: number
@@ -172,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. */
@@ -247,27 +260,30 @@ export class SessionQuerySqlite extends SessionQueryService {
override async searchEvents(
request: SessionEventSearchRequest,
exec?: SessionSearchExecContext,
): Promise<SessionSearchPage<SessionEventSearchHit>> {
): Promise<SessionEventSearchPage> {
const normalized = normalizeEventRequest(request, this.config)
const signal = exec?.signal
return this._serialized(signal, async () => {
await this._ensureReady(signal)
const persistenceBinding = await this._reconcile(signal)
assertNotAborted(signal)
const generation = this._targetGeneration(normalized.sessionId, persistenceBinding)
const target = this._targetObservation(normalized.sessionId, persistenceBinding)
const fingerprint = requestFingerprint(normalized)
const offset = normalized.cursor === undefined
? 0
: decodeCursor(normalized.cursor, this._instance, 'events', fingerprint, generation)
: decodeCursor(normalized.cursor, this._instance, 'events', fingerprint, target.generation)
const rows = this._queryEvents(normalized, offset, persistenceBinding)
return page(rows, normalized.limit, row => this._eventHit(row), cursorOffset => encodeCursor({
version: 1,
instance: this._instance,
scope: 'events',
fingerprint,
generation,
offset: cursorOffset,
}), offset)
return {
session: target.header,
...page(rows, normalized.limit, row => this._eventHit(row), cursorOffset => encodeCursor({
version: 1,
instance: this._instance,
scope: 'events',
fingerprint,
generation: target.generation,
offset: cursorOffset,
}), offset),
}
})
}
@@ -336,6 +352,7 @@ export class SessionQuerySqlite extends SessionQueryService {
}
private async _reconcile(signal: AbortSignal | undefined): Promise<PersistenceBinding> {
assertNotAborted(signal)
const db = this._requireDb()
const persistedRows = db.prepare(
'SELECT id, revision, generation FROM persisted_sessions',
@@ -436,7 +453,8 @@ export class SessionQuerySqlite extends SessionQueryService {
try {
const canReuseIndexed = this._lastPersistenceIdentity === undefined
|| this._lastPersistenceIdentity === persistenceBinding.identity
const before = await waitWithAbort(persistence.listSnapshots(), signal)
const before = await persistence.listSnapshots(signal)
assertNotAborted(signal)
persisted = materializePersistenceSnapshots(before)
for (const entry of persisted.values()) {
if (canReuseIndexed && indexed.get(entry.header.id)?.revision === entry.revision) continue
@@ -445,13 +463,16 @@ export class SessionQuerySqlite extends SessionQueryService {
// crash-repair side effects; the live-membership retry below makes
// the returned observation live-preferred.
if (initiallyLive.has(entry.header.id) || this.ctx.sessions.get(entry.header.id) !== undefined) continue
const loaded = await waitWithAbort(persistence.inspect(entry.header.id), signal)
assertNotAborted(signal)
const loaded = await persistence.inspect(entry.header.id, signal)
assertNotAborted(signal)
assertSessionHeadersCompatible(entry.header, loaded.meta)
entry.loaded = observeSession(loaded.meta, loaded.events)
}
const after = materializePersistenceSnapshots(
await waitWithAbort(persistence.listSnapshots(), signal),
)
assertNotAborted(signal)
const afterSnapshots = await persistence.listSnapshots(signal)
assertNotAborted(signal)
const after = materializePersistenceSnapshots(afterSnapshots)
if (!samePersistenceSnapshots(persisted, after)) continue
if (this._persistenceBinding !== persistenceBinding) continue
} catch (error: unknown) {
@@ -643,17 +664,33 @@ export class SessionQuerySqlite extends SessionQueryService {
`).all(...bindings) as unknown as SearchRow[]
}
private _targetGeneration(sessionId: SessionId, persistenceBinding: PersistenceBinding): string {
private _targetObservation(
sessionId: SessionId,
persistenceBinding: PersistenceBinding,
): { header: SessionHeader; generation: string } {
const db = this._requireDb()
const live = db.prepare(
'SELECT generation FROM temp.live_sessions WHERE id = ?',
).get(sessionId) as { generation: number } | undefined
if (live !== undefined) return `live:${live.generation}`
`SELECT
id AS session_id, version, created_at, cwd, parent_session, seed_length, delegation_depth, generation
FROM temp.live_sessions
WHERE id = ?`,
).get(sessionId) as (SessionHeaderRow & { generation: number }) | undefined
if (live !== undefined) {
return { header: rowHeader(live), generation: `live:${live.generation}` }
}
if (persistenceBinding.service !== undefined) {
const persisted = db.prepare(
'SELECT generation FROM persisted_sessions WHERE id = ?',
).get(sessionId) as { generation: number } | undefined
if (persisted !== undefined) return `persisted:${this._persistenceEpoch}:${persisted.generation}`
`SELECT
id AS session_id, version, created_at, cwd, parent_session, seed_length, delegation_depth, generation
FROM persisted_sessions
WHERE id = ?`,
).get(sessionId) as (SessionHeaderRow & { generation: number }) | undefined
if (persisted !== undefined) {
return {
header: rowHeader(persisted),
generation: `persisted:${this._persistenceEpoch}:${persisted.generation}`,
}
}
}
throw new SessionQueryError(
`session "${sessionId}" not found`,
@@ -835,7 +872,7 @@ function sameHeader(a: SessionHeader, b: SessionHeader): boolean {
&& (a.delegationDepth ?? 0) === (b.delegationDepth ?? 0)
}
function rowHeader(row: SearchRow): SessionHeader {
function rowHeader(row: SessionHeaderRow): SessionHeader {
return {
version: row.version,
id: row.session_id as SessionId,
@@ -914,6 +951,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')
@@ -924,6 +963,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,
@@ -68,11 +69,16 @@ class TestPersistence extends SessionPersistence {
static nextRevision = 0
static loads = new Map<SessionIdType, number>()
static inspections = new Map<SessionIdType, number>()
static inspectSignals: Array<AbortSignal | undefined> = []
static snapshotSignals: Array<AbortSignal | undefined> = []
static loadEffect: ((entry: { meta: SessionHeader; events: SessionEvent[] }) => void) | undefined
static inspectEffect: ((entry: { meta: SessionHeader; events: SessionEvent[] }) => void | Promise<void>) | undefined
static inspectEffect: ((
entry: { meta: SessionHeader; events: SessionEvent[] },
signal?: AbortSignal,
) => void | Promise<void>) | undefined
static listGate: Promise<void> | undefined
static listStarted: (() => void) | undefined
static snapshotEffect: (() => void | Promise<void>) | undefined
static snapshotEffect: ((signal?: AbortSignal) => void | Promise<void>) | undefined
static snapshotOverride: (() => SessionPersistenceSnapshot[]) | undefined
static failure: unknown
@@ -85,6 +91,8 @@ class TestPersistence extends SessionPersistence {
this.revisions = new Map()
this.loads = new Map()
this.inspections = new Map()
this.inspectSignals = []
this.snapshotSignals = []
this.loadEffect = undefined
this.inspectEffect = undefined
for (const entry of entries) this.set(entry)
@@ -127,12 +135,13 @@ class TestPersistence extends SessionPersistence {
return structuredClone(entry)
}
async inspect(id: SessionIdType): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
async inspect(id: SessionIdType, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
TestPersistence.inspections.set(id, (TestPersistence.inspections.get(id) ?? 0) + 1)
TestPersistence.inspectSignals.push(signal)
if (TestPersistence.failure !== undefined) throw TestPersistence.failure
const entry = TestPersistence.entries.get(id)
if (entry === undefined) throw new Error('missing test session')
await TestPersistence.inspectEffect?.(entry)
await TestPersistence.inspectEffect?.(entry, signal)
TestPersistence.inspectEffect = undefined
return structuredClone(entry)
}
@@ -145,7 +154,8 @@ class TestPersistence extends SessionPersistence {
}
async listSnapshots(): Promise<SessionPersistenceSnapshot[]> {
async listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]> {
TestPersistence.snapshotSignals.push(signal)
TestPersistence.listStarted?.()
await TestPersistence.listGate
if (TestPersistence.failure !== undefined) throw TestPersistence.failure
@@ -154,7 +164,7 @@ class TestPersistence extends SessionPersistence {
header: structuredClone(entry.meta),
revision: SessionPersistenceRevision(`test:${TestPersistence.revisions.get(entry.meta.id)}`),
}))
await TestPersistence.snapshotEffect?.()
await TestPersistence.snapshotEffect?.(signal)
return snapshots
}
}
@@ -167,6 +177,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'), {
@@ -179,7 +212,10 @@ describe('SQLite session search', () => {
)
await expect(ctx.sessionQuery.searchEvents({ sessionId: session.id, query: 'AI' }))
.resolves.toMatchObject({ items: [{ sessionId: session.id, seq: 0, snippet: 'An AI helper' }] })
.resolves.toMatchObject({
session: { ...session.header, seedLength: 1 },
items: [{ sessionId: session.id, seq: 0, snippet: 'An AI helper' }],
})
await expect(ctx.sessionQuery.searchSessions({ query: 'AI' }))
.resolves.toMatchObject({ items: [{ header: { ...session.header, seedLength: 1 }, live: true, persisted: false }] })
})
@@ -483,6 +519,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' },
]) {
@@ -1200,6 +1238,167 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
}
})
it.each(['sessions', 'events'] as const)(
'forwards one exact reconciliation signal through both snapshot lists and persisted inspection for %s search',
async (scope) => {
const durable = header(`signal-${scope}`)
TestPersistence.reset([{ meta: durable, events: messageEvents('signal needle') }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const result = scope === 'sessions'
? await ctx.sessionQuery.searchSessions({ query: 'needle' }, { signal: controller.signal })
: await ctx.sessionQuery.searchEvents(
{ sessionId: durable.id, query: 'needle' },
{ signal: controller.signal },
)
expect(result.items).toHaveLength(1)
expect(TestPersistence.snapshotSignals).toEqual([controller.signal, controller.signal])
expect(TestPersistence.inspectSignals).toEqual([controller.signal])
},
)
it.each(['sessions', 'events'] as const)(
'starts no persistence observation for a pre-aborted %s search',
async (scope) => {
const durable = header(`pre-aborted-${scope}`)
TestPersistence.reset([{ meta: durable, events: messageEvents('needle') }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
controller.abort(new Error(`pre-aborted ${scope}`))
const pending = scope === 'sessions'
? ctx.sessionQuery.searchSessions({ query: 'needle' }, { signal: controller.signal })
: ctx.sessionQuery.searchEvents(
{ sessionId: durable.id, query: 'needle' },
{ signal: controller.signal },
)
await expect(pending).rejects.toThrow(expectCode('SESSION_QUERY_ABORTED'))
expect(TestPersistence.snapshotSignals).toEqual([])
expect(TestPersistence.inspectSignals).toEqual([])
},
)
it('awaits cooperative snapshot-list cancellation cleanup without starting another observation step', async () => {
const durable = header('cooperative-list-abort')
TestPersistence.reset([{ meta: durable, events: messageEvents('needle') }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const started = Promise.withResolvers<AbortSignal>()
const abortObserved = Promise.withResolvers<undefined>()
const cleanup = Promise.withResolvers<undefined>()
TestPersistence.snapshotEffect = async (signal) => {
TestPersistence.snapshotEffect = undefined
if (signal === undefined) throw new Error('expected reconciliation signal')
started.resolve(signal)
await new Promise<void>((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
abortObserved.resolve(undefined)
await cleanup.promise
signal.throwIfAborted()
}
const controller = new AbortController()
const pending = ctx.sessionQuery.searchSessions({ query: 'needle' }, { signal: controller.signal })
expect(await started.promise).toBe(controller.signal)
let settled = false
void pending.then(
() => { settled = true },
() => { settled = true },
)
controller.abort(new Error('cooperative list cancellation'))
await abortObserved.promise
expect(settled).toBe(false)
expect(TestPersistence.snapshotSignals).toEqual([controller.signal])
expect(TestPersistence.inspectSignals).toEqual([])
cleanup.resolve(undefined)
await expect(pending).rejects.toThrow(expectCode('SESSION_QUERY_ABORTED'))
})
it('keeps a second search serialized while an abort-ignoring snapshot list finishes', async () => {
const durable = header('serialized-list-abort')
TestPersistence.reset([{ meta: durable, events: messageEvents('needle') }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const cleanup = Promise.withResolvers<undefined>()
const started = Promise.withResolvers<undefined>()
TestPersistence.listGate = cleanup.promise
TestPersistence.listStarted = () => {
TestPersistence.listStarted = undefined
started.resolve(undefined)
}
const controller = new AbortController()
const first = ctx.sessionQuery.searchSessions({ query: 'needle' }, { signal: controller.signal })
await started.promise
let firstSettled = false
let secondSettled = false
void first.then(
() => { firstSettled = true },
() => { firstSettled = true },
)
controller.abort(new Error('ignored list cancellation'))
const second = ctx.sessionQuery.searchEvents({ sessionId: durable.id, query: 'needle' })
void second.then(
() => { secondSettled = true },
() => { secondSettled = true },
)
await Promise.resolve()
expect(firstSettled).toBe(false)
expect(secondSettled).toBe(false)
expect(TestPersistence.snapshotSignals).toEqual([controller.signal])
expect(TestPersistence.inspectSignals).toEqual([])
cleanup.resolve(undefined)
await expect(first).rejects.toThrow(expectCode('SESSION_QUERY_ABORTED'))
await expect(second).resolves.toMatchObject({ items: [{ sessionId: durable.id }] })
})
it('awaits an abort-ignoring inspection and starts neither another inspection nor the after-list', async () => {
const first = header('ignored-inspect-first')
const second = header('ignored-inspect-second')
TestPersistence.reset([
{ meta: first, events: messageEvents('first needle') },
{ meta: second, events: messageEvents('second needle') },
])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const started = Promise.withResolvers<AbortSignal>()
const cleanup = Promise.withResolvers<undefined>()
TestPersistence.inspectEffect = async (_entry, signal) => {
TestPersistence.inspectEffect = undefined
if (signal === undefined) throw new Error('expected reconciliation signal')
started.resolve(signal)
await cleanup.promise
}
const controller = new AbortController()
const pending = ctx.sessionQuery.searchSessions({ query: 'needle' }, { signal: controller.signal })
expect(await started.promise).toBe(controller.signal)
let settled = false
void pending.then(
() => { settled = true },
() => { settled = true },
)
controller.abort(new Error('ignored inspect cancellation'))
await Promise.resolve()
expect(settled).toBe(false)
expect(TestPersistence.snapshotSignals).toEqual([controller.signal])
expect(TestPersistence.inspections.get(first.id)).toBe(1)
expect(TestPersistence.inspections.get(second.id)).toBeUndefined()
cleanup.resolve(undefined)
await expect(pending).rejects.toThrow(expectCode('SESSION_QUERY_ABORTED'))
expect(TestPersistence.snapshotSignals).toEqual([controller.signal])
expect(TestPersistence.inspections.get(second.id)).toBeUndefined()
})
it('cancels both queued and in-flight source waits without committing them', async () => {
TestPersistence.reset()
const ctx = await liveContext()
@@ -1253,8 +1452,15 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
const active = ctx.sessionQuery.searchSessions({ query: 'needle' }, { signal: activeController.signal })
await activeStarted
activeController.abort()
await expect(active).rejects.toThrow(expectCode('SESSION_QUERY_ABORTED'))
let activeSettled = false
void active.then(
() => { activeSettled = true },
() => { activeSettled = true },
)
await Promise.resolve()
expect(activeSettled).toBe(false)
releaseActive()
await expect(active).rejects.toThrow(expectCode('SESSION_QUERY_ABORTED'))
const db = (ctx.sessionQuery as unknown as { _db: DatabaseSync })._db
expect(db.prepare('SELECT COUNT(*) AS count FROM persisted_sessions').get()).toEqual({ count: 0 })
@@ -1262,6 +1468,57 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
.resolves.toMatchObject({ items: [{ header: { id: SessionId('uncommitted') } }] })
})
it.each([
[new Error('ready error'), 'ready error'],
['non-error ready failure', 'session-search dependency rejected with a non-Error value'],
])('normalizes a rejected readiness wait before mapping it to an index error', async (failure, detail) => {
TestPersistence.reset()
const ctx = await liveContext()
const internals = ctx.sessionQuery as unknown as {
_ready: Promise<void>
_ensureReady(signal: AbortSignal): Promise<void>
}
internals._ready = Promise.resolve().then(() => {
throw failure
})
await expect(internals._ensureReady(new AbortController().signal))
.rejects.toThrow(`session-search SQLite index failed to open: ${detail}`)
})
it('checks cancellation after readiness before reconciliation accesses SQLite', async () => {
TestPersistence.reset()
const ctx = await liveContext()
const internals = ctx.sessionQuery as unknown as {
_db: DatabaseSync
_ready: Promise<void>
_ensureReady(signal: AbortSignal | undefined): Promise<void>
}
const readiness = Promise.withResolvers<undefined>()
internals._ready = readiness.promise
const readyWaitStarted = Promise.withResolvers<undefined>()
const ensureReady = internals._ensureReady.bind(internals)
vi.spyOn(internals, '_ensureReady').mockImplementation(async (signal) => {
const pending = ensureReady(signal)
readyWaitStarted.resolve(undefined)
return pending
})
const prepare = vi.spyOn(internals._db, 'prepare')
const reason = new Error('cancelled after readiness')
const controller = new AbortController()
const pending = ctx.sessionQuery.searchSessions({ query: 'needle' }, { signal: controller.signal })
await readyWaitStarted.promise
const queueBoundaryAbort = readiness.promise.then(() => {
queueMicrotask(() => { controller.abort(reason) })
})
readiness.resolve(undefined)
await queueBoundaryAbort
await expect(pending).rejects.toThrow(expectCode('SESSION_QUERY_ABORTED'))
expect(prepare).not.toHaveBeenCalled()
})
it('rejects queued and future work when close waits for an accepted operation', async () => {
TestPersistence.reset()
let release!: () => void
@@ -1327,7 +1584,7 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
await expect(ctx.sessionQuery.searchSessions({ query: 'SQLite needle' }))
.resolves.toMatchObject({ items: [{ header: meta, persisted: true, live: false }] })
await expect(ctx.sessionQuery.searchEvents({ sessionId: meta.id, query: 'SQLite needle' }))
.resolves.toMatchObject({ items: [{ sessionId: meta.id, seq: 0 }] })
.resolves.toMatchObject({ session: meta, items: [{ sessionId: meta.id, seq: 0 }] })
await expect(ctx.sessionQuery.searchEvents({ sessionId: SessionId('absent'), query: 'needle' }))
.rejects.toThrow(expectCode('SESSION_QUERY_SESSION_NOT_FOUND'))
await search.dispose()

View File

@@ -4,18 +4,18 @@
## Reads
- `listSessions()` reads current persistence metadata, merges live records with live precedence, and returns cloned records in deterministic newest-first order.
- `listSessions(signal?)` reads current persistence metadata, merges live records with live precedence, and returns cloned records in deterministic newest-first order.
- `readSession(sessionId)` returns one complete detached raw log after the same core replay validation used by resume; it never enters the session into the live store.
- `filterSessions(filters)` applies provider-independent session metadata and availability predicates to that same cloned logical corpus.
- `filterSessions(filters, signal?)` applies provider-independent session metadata and availability predicates to that same cloned logical corpus.
- `filterEvents(sessionId, filters)` extracts first-party semantic documents and applies provider-independent metadata and literal-text predicates in ascending seq order.
- `readTitle(sessionId)` loads one live-preferred or persisted log and folds its latest `session/title` event into a `SessionTitleSnapshot`; it returns `undefined` when the known session has no title.
- `readTitleSnapshots(sessionIds, signal?)` resolves unique ids from one live-preferred corpus observation, passes cancellation through persisted listing and inspection, and returns ordered per-session settlements so one missing or malformed title source does not discard its peers. Each live source is folded directly, and each persisted worker folds to a detached header/title result and releases the full log before dequeuing another id. Cancellation rejects the whole batch. `readTitleSnapshot(sessionId, signal?)` is the one-observation view; `readTitle(sessionId, signal?)` returns only its optional folded `session/title`.
- `listEvents(sessionId)` loads the live-preferred raw log and classifies each event as `current`, `shadowed`, or `log-only` with the shared `dsh-session` surface fold.
- `readSurface(sessionId)` returns one cloned header, raw-log capture boundary, and the complete folded current surface in model-history order. A live session wins over persistence; compaction is observed before or after its replacement append, never as a synthetic mixture.
- `readEvent(request)` returns a cloned header, the full target event, and a bounded raw-seq window. `before` and `after` default to zero and may not exceed `readWindowMax`.
- `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 direct positional replacements and direct logged provenance. `replacementChain` follows positional replacers to the final replacement; provenance links remain non-transitive.
- `readEvent(request, signal?)` returns a cloned header, the full target event, and a bounded raw-seq window. `before` and `after` default to zero and may not exceed `readWindowMax`.
- `traceSession(sessionId, signal?)` 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, signal?)` 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. `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 read, event trace, or event read 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. Lineage-trace cancellation is passed to persisted listing; event-trace and event-read cancellation is passed to persisted listing and inspection. Each waits for the started backend call to settle, then rejects with the signal's exact reason even when the backend ignored that signal. A pre-aborted known-live title read, event trace, or event read rejects before folding or snapshotting without consulting persistence. 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
@@ -25,7 +25,7 @@ The text clause is deliberately independent of FTS providers: caller text is esc
## Full-text methods
`SessionQueryService.searchSessions(request, exec?)` groups the logical corpus by strongest matching event; `searchEvents(request, exec?)` searches one logical session. These are the service's only abstract methods. Both return pages whose continuation is an owned branded `SessionSearchCursor`, accept optional cancellation, and expose snippets without provider-specific numeric scores. Search requests accept only metadata event filters, because literal-text filtering is the scan path described above.
`SessionQueryService.searchSessions(request, exec?)` groups the logical corpus by strongest matching event; `searchEvents(request, exec?)` searches one logical session. These are the service's only abstract methods. Both return pages whose continuation is an owned branded `SessionSearchCursor`, accept optional cancellation, and expose snippets without provider-specific numeric scores. An event-search page also carries the cloned target header from the same indexed generation as its hits, allowing authorization consumers to bind policy to the payload observation. Search requests accept only metadata event filters, because literal-text filtering is the scan path described above.
The package has no provider coordinator, fallback implementation, or standalone concrete plugin. A concrete service backend inherits the implemented reads, filters, and traces while owning full-text observation, reconciliation, ranking, cursor generations, and query execution; the first implementation is [`@deepseek-ai/dsh-session-query-sqlite`](../session-query-sqlite/README.md).
@@ -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

@@ -15,12 +15,28 @@ export interface LogicalSession {
events: SessionEvent[]
}
/** Borrowed source visible only during one synchronous batch projection. */
export interface LogicalSessionSource {
/** Header selected with `events`; callers must clone retained output. */
readonly header: SessionHeader
/** Raw events selected with `header`; valid only for the projection call. */
readonly events: readonly SessionEvent[]
}
/** One source-projection result in a batch logical-corpus observation. */
export type LogicalProjectionResult<Value> =
| { sessionId: SessionId; status: 'fulfilled'; value: Value }
| { sessionId: SessionId; status: 'rejected'; reason: unknown }
/** 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
@@ -36,11 +52,14 @@ export class SessionCorpus {
/**
* List the complete logical corpus with live precedence and cloned headers.
* @param signal - optional cancellation for persistence listing.
* @returns records in deterministic newest-first order.
*/
async listSessions(): Promise<SessionRecord[]> {
async listSessions(signal?: AbortSignal): Promise<SessionRecord[]> {
signal?.throwIfAborted()
const persistence = this._persistence
const persisted = persistence === undefined ? [] : await listPersisted(persistence)
const persisted = persistence === undefined ? [] : await listPersisted(persistence, signal)
signal?.throwIfAborted()
const records = new Map<SessionId, SessionRecord>()
for (const header of persisted) {
records.set(header.id, { header: structuredClone(header), live: false, persisted: true })
@@ -63,39 +82,181 @@ export class SessionCorpus {
* A known live target never consults persistence, so an optional backend's
* failure cannot make current in-memory history unreadable.
* @param sessionId - session to resolve.
* @param signal - optional cancellation for persisted source resolution.
* @returns detached live-preferred header and events.
*/
async load(sessionId: SessionId): Promise<LogicalSession> {
async load(sessionId: SessionId, signal?: AbortSignal): Promise<LogicalSession> {
signal?.throwIfAborted()
const live = this._ctx.sessions.get(sessionId)
if (live !== undefined) return snapshotLive(live)
if (live !== undefined) {
const snapshot = snapshotLive(live)
signal?.throwIfAborted()
return snapshot
}
const persistence = this._persistence
if (persistence === undefined) throw notFound(sessionId)
const listed = (await listPersisted(persistence)).find(header => header.id === sessionId)
const listed = (await listPersisted(persistence, signal)).find(header => header.id === sessionId)
signal?.throwIfAborted()
if (listed === undefined) throw notFound(sessionId)
let loaded: Awaited<ReturnType<SessionPersistence['inspect']>>
try {
loaded = await persistence.inspect(sessionId)
} catch (error: unknown) {
throw new SessionQueryError(
`failed to inspect session "${sessionId}": ${errorMessage(error)}`,
'SESSION_QUERY_PERSISTENCE_FAILED',
{ cause: error },
)
}
const loaded = await inspectPersisted(persistence, sessionId, signal)
signal?.throwIfAborted()
const attached = this._ctx.sessions.get(sessionId)
if (attached !== undefined) return snapshotLive(attached)
if (attached !== undefined) {
const snapshot = snapshotLive(attached)
signal?.throwIfAborted()
return snapshot
}
assertSessionHeadersCompatible(loaded.meta, listed)
return {
const snapshot = {
header: structuredClone(loaded.meta),
events: loaded.events.map(event => structuredClone(event)),
}
signal?.throwIfAborted()
return snapshot
}
/**
* Project unique logical sources immediately from one persistence listing.
*
* The synchronous projector runs before a persisted worker claims its next id.
* Full logs are borrowed only for that call and never retained by the batch.
* @param sessionIds - sessions to resolve in first-occurrence order.
* @param project - synchronous fold that owns/clones every retained value.
* @param signal - cancellation shared by listing and every persisted inspection.
* @returns one fulfilled or rejected projected result per unique requested id.
*/
async projectMany<Value>(
sessionIds: readonly SessionId[],
project: (source: LogicalSessionSource) => Value,
signal?: AbortSignal,
): Promise<LogicalProjectionResult<Value>[]> {
const ids = [...new Set(sessionIds)]
signal?.throwIfAborted()
const resolved = new Map<SessionId, LogicalProjectionResult<Value>>()
const unresolved: SessionId[] = []
for (const id of ids) {
const session = this._ctx.sessions.get(id)
if (session === undefined) {
unresolved.push(id)
} else {
resolved.set(id, projectSource(id, sourceLive(session), project, signal))
}
}
if (unresolved.length === 0) return orderedResults(ids, resolved)
const persistence = this._persistence
if (persistence === undefined) {
for (const sessionId of unresolved) {
resolved.set(sessionId, { sessionId, status: 'rejected', reason: notFound(sessionId) })
}
return orderedResults(ids, resolved)
}
let persisted: SessionHeader[]
try {
persisted = await listPersisted(persistence, signal)
signal?.throwIfAborted()
} catch (error: unknown) {
if (signal?.aborted) signal.throwIfAborted()
for (const sessionId of unresolved) {
resolved.set(sessionId, { sessionId, status: 'rejected', reason: error })
}
return orderedResults(ids, resolved)
}
const persistedById = new Map(persisted.map(header => [header.id, header]))
const resolvePersisted = async (sessionId: SessionId): Promise<void> => {
const listed = persistedById.get(sessionId)
if (listed === undefined) {
const attached = this._ctx.sessions.get(sessionId)
resolved.set(sessionId, attached === undefined
? { sessionId, status: 'rejected', reason: notFound(sessionId) }
: projectSource(sessionId, sourceLive(attached), project, signal))
return
}
try {
signal?.throwIfAborted()
const loaded = await inspectPersisted(persistence, sessionId, signal)
signal?.throwIfAborted()
const attached = this._ctx.sessions.get(sessionId)
if (attached !== undefined) {
resolved.set(sessionId, projectSource(sessionId, sourceLive(attached), project, signal))
return
}
assertSessionHeadersCompatible(loaded.meta, listed)
resolved.set(sessionId, projectSource(sessionId, {
header: loaded.meta,
events: loaded.events,
}, project, signal))
} catch (error: unknown) {
if (signal?.aborted) signal.throwIfAborted()
resolved.set(sessionId, { sessionId, status: 'rejected', reason: error })
}
}
let cursor = 0
const worker = async (): Promise<void> => {
for (;;) {
signal?.throwIfAborted()
const index = cursor
if (index >= unresolved.length) return
cursor += 1
await resolvePersisted(unresolved[index] as SessionId)
}
}
const workerCount = Math.min(this._persistedInspectConcurrency, unresolved.length)
const settlements = await Promise.allSettled(
Array.from({ length: workerCount }, () => worker()),
)
if (signal?.aborted) signal.throwIfAborted()
/* v8 ignore start -- per-id failures settle inside resolvePersisted; workers reject only on abort above */
for (const settlement of settlements) {
if (settlement.status === 'rejected') {
const reason: unknown = settlement.reason
throw reason
}
}
/* v8 ignore stop */
signal?.throwIfAborted()
return orderedResults(ids, resolved)
}
}
async function listPersisted(persistence: SessionPersistence): Promise<SessionHeader[]> {
function projectSource<Value>(
sessionId: SessionId,
source: LogicalSessionSource,
project: (source: LogicalSessionSource) => Value,
signal?: AbortSignal,
): LogicalProjectionResult<Value> {
try {
return await persistence.list()
signal?.throwIfAborted()
const value = project(source)
signal?.throwIfAborted()
return { sessionId, status: 'fulfilled', value }
} catch (reason: unknown) {
/* v8 ignore next -- the synchronous projector has no external cancellation yield */
if (signal?.aborted) signal.throwIfAborted()
return { sessionId, status: 'rejected', reason }
}
}
function sourceLive(session: Session): LogicalSessionSource {
return { header: session.header, events: session.events }
}
function orderedResults<Value>(
ids: readonly SessionId[],
resolved: ReadonlyMap<SessionId, LogicalProjectionResult<Value>>,
): LogicalProjectionResult<Value>[] {
return ids.map(sessionId => resolved.get(sessionId) as LogicalProjectionResult<Value>)
}
async function listPersisted(
persistence: SessionPersistence,
signal?: AbortSignal,
): Promise<SessionHeader[]> {
try {
return await persistence.list(signal)
} catch (error: unknown) {
if (signal?.aborted) signal.throwIfAborted()
throw new SessionQueryError(
`session persistence listing failed: ${errorMessage(error)}`,
'SESSION_QUERY_PERSISTENCE_FAILED',
@@ -104,6 +265,23 @@ async function listPersisted(persistence: SessionPersistence): Promise<SessionHe
}
}
async function inspectPersisted(
persistence: SessionPersistence,
sessionId: SessionId,
signal?: AbortSignal,
): Promise<Awaited<ReturnType<SessionPersistence['inspect']>>> {
try {
return await persistence.inspect(sessionId, signal)
} catch (error: unknown) {
if (signal?.aborted) signal.throwIfAborted()
throw new SessionQueryError(
`failed to inspect session "${sessionId}": ${errorMessage(error)}`,
'SESSION_QUERY_PERSISTENCE_FAILED',
{ cause: error },
)
}
}
function snapshotLive(session: Session): LogicalSession {
return {
header: structuredClone(session.header),

View File

@@ -10,12 +10,12 @@ import { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
import type { SessionTitleSnapshot } from '@deepseek-ai/dsh-session-title'
import type {
SessionEventResultFilter,
SessionEventSearchPage,
SessionEventReadRequest,
SessionEventRecord,
SessionEventSearchHit,
SessionEventSearchDocument,
SessionEventSearchRequest,
SessionEventTrace,
SessionEventTraceObservation,
SessionEventTraceRequest,
SessionEventWindow,
SessionLineageTrace,
@@ -27,8 +27,11 @@ import type {
SessionSearchPage,
SessionSearchRequest,
SessionSurfaceSnapshot,
SessionTitleObservation,
SessionTitleObservationResult,
} from './types.ts'
import {
SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY,
SESSION_QUERY_READ_WINDOW_MAX,
SessionQueryError,
type Config,
@@ -46,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 {
@@ -86,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)
}
/**
@@ -104,19 +119,20 @@ export abstract class SessionQueryService extends Service {
* Search events within one live-preferred logical session.
* @param request - target session, query text, filters, page size, and cursor.
* @param exec - optional cancellation control.
* @returns matching event hits in deterministic relevance order.
* @returns matching event hits and their target header from one indexed generation.
*/
abstract searchEvents(
request: SessionEventSearchRequest,
exec?: SessionSearchExecContext,
): Promise<SessionSearchPage<SessionEventSearchHit>>
): Promise<SessionEventSearchPage>
/**
* List the complete logical corpus using live-preferred records.
* @param signal - optional cancellation for persistence listing.
* @returns deterministic newest-first cloned session records.
*/
listSessions(): Promise<SessionRecord[]> {
return this._corpus.listSessions()
listSessions(signal?: AbortSignal): Promise<SessionRecord[]> {
return this._corpus.listSessions(signal)
}
/**
@@ -137,21 +153,65 @@ export abstract class SessionQueryService extends Service {
/**
* Filter the complete logical corpus with provider-independent predicates.
* @param filters - ANDed session metadata and availability clauses.
* @param signal - optional cancellation for persistence listing.
* @returns matching cloned records in deterministic newest-first order.
*/
async filterSessions(filters: readonly SessionResultFilter[]): Promise<SessionRecord[]> {
async filterSessions(
filters: readonly SessionResultFilter[],
signal?: AbortSignal,
): Promise<SessionRecord[]> {
const ownedFilters = materializeSessionResultFilters(filters)
return this._filterSessions(ownedFilters)
return this._filterSessions(ownedFilters, signal)
}
/**
* Fold the latest log-backed title from one live-preferred logical session.
* @param sessionId - live or persisted session id to read.
* @param signal - optional cancellation for source resolution and title folding.
* @returns latest title snapshot, or `undefined` when the log has no title event.
*/
async readTitle(sessionId: SessionId): Promise<SessionTitleSnapshot | undefined> {
const loaded = await this._corpus.load(sessionId)
return foldSessionTitle(loaded.events)
async readTitle(
sessionId: SessionId,
signal?: AbortSignal,
): Promise<SessionTitleSnapshot | undefined> {
return (await this.readTitleSnapshot(sessionId, signal)).title
}
/**
* Fold the latest title and return its source header from one corpus observation.
* @param sessionId - live or persisted session id to read.
* @param signal - optional cancellation for source resolution and title folding.
* @returns cloned source header and optional latest title snapshot.
*/
async readTitleSnapshot(
sessionId: SessionId,
signal?: AbortSignal,
): Promise<SessionTitleObservation> {
const result = (await this.readTitleSnapshots([sessionId], signal))[0] as SessionTitleObservationResult
if (result.status === 'rejected') throw result.reason
return result.value
}
/**
* Fold titles for unique sessions from one cancellable corpus observation.
*
* Results preserve first-occurrence input order. Operational failures stay
* isolated per session, while cancellation rejects the complete operation.
* @param sessionIds - live or persisted session ids to observe.
* @param signal - optional cancellation shared by all source reads.
* @returns one fulfilled or rejected result per unique requested id.
*/
async readTitleSnapshots(
sessionIds: readonly SessionId[],
signal?: AbortSignal,
): Promise<SessionTitleObservationResult[]> {
return this._corpus.projectMany(sessionIds, (source): SessionTitleObservation => {
const title = foldSessionTitle(source.events)
return {
session: structuredClone(source.header),
...title === undefined ? {} : { title },
}
}, signal)
}
/**
@@ -178,8 +238,11 @@ export abstract class SessionQueryService extends Service {
return this._filterEvents(sessionId, ownedFilters)
}
private async _filterSessions(filters: readonly SessionResultFilter[]): Promise<SessionRecord[]> {
return filterSessionResults(await this._corpus.listSessions(), filters)
private async _filterSessions(
filters: readonly SessionResultFilter[],
signal?: AbortSignal,
): Promise<SessionRecord[]> {
return filterSessionResults(await this._corpus.listSessions(signal), filters)
}
private async _filterEvents(
@@ -209,36 +272,44 @@ export abstract class SessionQueryService extends Service {
/**
* Trace known ancestry and descendants from one corpus observation.
* @param sessionId - logical session id to trace.
* @param signal - optional cancellation for persistence listing.
* @returns a complete lineage or an explicit unresolved parent boundary.
* @throws when corpus resolution fails, the target is absent, or its known ancestry cycles.
*/
async traceSession(sessionId: SessionId): Promise<SessionLineageTrace> {
const records = await this._corpus.listSessions()
async traceSession(sessionId: SessionId, signal?: AbortSignal): Promise<SessionLineageTrace> {
const records = await this._corpus.listSessions(signal)
signal?.throwIfAborted()
return tracing.traceSession(records, sessionId)
}
/**
* Trace one event's direct positional and provenance relationships.
* @param request - target session id and event seq.
* @returns direct links plus the target's positional replacement chain.
* @param signal - optional cancellation for persisted source resolution.
* @returns source header, direct links, and the target's positional replacement chain.
* @throws when source resolution fails, the target is absent, or surface/provenance validation fails.
*/
async traceEvent(request: SessionEventTraceRequest): Promise<SessionEventTrace> {
const loaded = await this._corpus.load(request.sessionId)
return tracing.traceEvent(request.sessionId, loaded.events, request.seq)
async traceEvent(request: SessionEventTraceRequest, signal?: AbortSignal): Promise<SessionEventTraceObservation> {
const loaded = await this._corpus.load(request.sessionId, signal)
signal?.throwIfAborted()
return {
session: loaded.header,
...tracing.traceEvent(request.sessionId, loaded.events, request.seq),
}
}
/**
* Read one full event plus a bounded raw-log context window.
* @param request - target session/seq and context sizes.
* @param signal - optional cancellation for persisted source resolution.
* @returns cloned target and neighboring events.
*/
async readEvent(request: SessionEventReadRequest): Promise<SessionEventWindow> {
async readEvent(request: SessionEventReadRequest, signal?: AbortSignal): Promise<SessionEventWindow> {
const before = this._readWindow('before', request.before)
const after = this._readWindow('after', request.after)
const sessionId = request.sessionId
const seq = request.seq
return this._readEvent(sessionId, seq, before, after)
return this._readEvent(sessionId, seq, before, after, signal)
}
private async _readEvent(
@@ -246,8 +317,10 @@ export abstract class SessionQueryService extends Service {
seq: number,
before: number,
after: number,
signal?: AbortSignal,
): Promise<SessionEventWindow> {
const loaded = await this._corpus.load(sessionId)
const loaded = await this._corpus.load(sessionId, signal)
signal?.throwIfAborted()
const target = loaded.events[seq]
if (target === undefined || target.seq !== seq) {
throw new SessionQueryError(

View File

@@ -12,6 +12,7 @@ import type {
SessionId,
SurfaceEvent,
} from '@deepseek-ai/dsh-session'
import type { SessionTitleSnapshot } from '@deepseek-ai/dsh-session-title'
import type { SessionSearchCursor } from './cursor.ts'
export type { SessionSearchCursor } from './cursor.ts'
@@ -116,6 +117,12 @@ export interface SessionEventTrace {
derivedEventSeqs: number[]
}
/** Event relationships bound to the same session-header observation. */
export interface SessionEventTraceObservation extends SessionEventTrace {
/** Cloned header selected with the event log used for the trace. */
session: SessionHeader
}
/** Request for one event plus raw neighboring log context. */
export interface SessionEventReadRequest {
/** Session that owns the target event. */
@@ -142,6 +149,33 @@ export interface SessionEventWindow {
endSeq: number
}
/** Latest folded title bound to the same session-header observation. */
export interface SessionTitleObservation {
/** Cloned header selected with the event log used for the title fold. */
session: SessionHeader
/** Latest title snapshot, absent when the observed log has no title. */
title?: SessionTitleSnapshot
}
/** One ordered result from a batch title observation. */
export type SessionTitleObservationResult =
| {
/** Requested session id. */
sessionId: SessionId
/** Successful atomic header/title observation. */
status: 'fulfilled'
/** Header and optional latest title from one logical source. */
value: SessionTitleObservation
}
| {
/** Requested session id. */
sessionId: SessionId
/** Operational failure isolated to this session. */
status: 'rejected'
/** Original failure from logical-source resolution or title folding. */
reason: unknown
}
/** Inclusive numeric interval used by time and sequence filters. */
export interface SessionResultRange {
/** Inclusive lower bound. */
@@ -192,6 +226,12 @@ export interface SessionSearchPage<T> {
nextCursor?: SessionSearchCursor
}
/** Event-search results bound to the indexed target-session observation. */
export interface SessionEventSearchPage extends SessionSearchPage<SessionEventSearchHit> {
/** Cloned target header from the same indexed generation as `items`. */
session: SessionHeader
}
/** Controls shared by cross-session and within-session search calls. */
export interface SessionSearchExecContext {
/** Abort caller waiting and interrupt provider work where supported. */

View File

@@ -210,8 +210,10 @@ it('registers exact and abstract search behavior under one ctx key', async () =>
const ctx = new Context()
await ctx.plugin(SessionStore)
const fiber = await ctx.plugin(TestSessionQueryService)
const session = ctx.sessions.create(id)
await expect(ctx.sessionQuery.searchSessions({ query: 'AI' })).resolves.toEqual({ items: [] })
await expect(ctx.sessionQuery.searchEvents({ sessionId: id, query: 'AI' })).resolves.toEqual({ items: [] })
await expect(ctx.sessionQuery.searchEvents({ sessionId: id, query: 'AI' }))
.resolves.toEqual({ session: session.header, items: [] })
await fiber.dispose()
expect(ctx.sessionQuery).toBeUndefined()
})

View File

@@ -1,9 +1,10 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { Context, type Fiber } from 'cordis'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
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'
@@ -27,16 +28,31 @@ function eventLog(text = 'hello'): SessionEvent[] {
class TestPersistence extends SessionPersistence {
static entries = new Map<SessionIdType, { meta: SessionHeader; events: SessionEvent[] }>()
static listFailure: unknown
static listOverride: ((signal?: AbortSignal) => Promise<SessionHeader[]>) | undefined
static inspectFailure: unknown
static inspectEffect: (() => void) | undefined
static inspectOverride: ((
id: SessionIdType,
signal?: AbortSignal,
) => Promise<{ meta: SessionHeader; events: SessionEvent[] }>) | undefined
static afterList: (() => void) | undefined
static listCalls = 0
static inspectCalls: SessionIdType[] = []
static listSignals: Array<AbortSignal | undefined> = []
static inspectSignals: Array<AbortSignal | undefined> = []
static reset(entries: readonly { meta: SessionHeader; events: SessionEvent[] }[] = []): void {
this.entries = new Map(entries.map(entry => [entry.meta.id, structuredClone(entry)]))
this.listFailure = undefined
this.listOverride = undefined
this.inspectFailure = undefined
this.inspectEffect = undefined
this.inspectOverride = undefined
this.afterList = undefined
this.listCalls = 0
this.inspectCalls = []
this.listSignals = []
this.inspectSignals = []
}
locate(_meta: SessionHeader): undefined {
@@ -59,7 +75,15 @@ class TestPersistence extends SessionPersistence {
return this.inspect(id)
}
inspect(id: SessionIdType): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
inspect(
id: SessionIdType,
signal?: AbortSignal,
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
TestPersistence.inspectCalls.push(id)
TestPersistence.inspectSignals.push(signal)
if (TestPersistence.inspectOverride !== undefined) {
return TestPersistence.inspectOverride(id, signal)
}
if (TestPersistence.inspectFailure !== undefined) return rejectUnknown(TestPersistence.inspectFailure)
const entry = TestPersistence.entries.get(id)
if (entry === undefined) return Promise.reject(new Error('missing test session'))
@@ -69,7 +93,10 @@ class TestPersistence extends SessionPersistence {
return Promise.resolve(result)
}
list(): Promise<SessionHeader[]> {
list(signal?: AbortSignal): Promise<SessionHeader[]> {
TestPersistence.listCalls += 1
TestPersistence.listSignals.push(signal)
if (TestPersistence.listOverride !== undefined) return TestPersistence.listOverride(signal)
if (TestPersistence.listFailure !== undefined) return rejectUnknown(TestPersistence.listFailure)
const headers = [...TestPersistence.entries.values()].map(entry => structuredClone(entry.meta))
TestPersistence.afterList?.()
@@ -104,6 +131,287 @@ function rejectUnknown<T>(reason: unknown): Promise<T> {
})
}
const cancellableSessionListings = [
{
name: 'listSessions',
run: (ctx: Context, signal: AbortSignal) => ctx.sessionQuery.listSessions(signal),
},
{
name: 'filterSessions',
run: (ctx: Context, signal: AbortSignal) => ctx.sessionQuery.filterSessions([], signal),
},
] as const
interface CancellableExactRead {
readonly name: 'traceSession' | 'traceEvent' | 'readEvent'
readonly inspects: boolean
readonly run: (
ctx: Context,
sessionId: SessionIdType,
signal: AbortSignal,
) => Promise<unknown>
}
const cancellableExactReads: readonly CancellableExactRead[] = [
{
name: 'traceSession',
inspects: false,
run: (ctx, sessionId, signal) => ctx.sessionQuery.traceSession(sessionId, signal),
},
{
name: 'traceEvent',
inspects: true,
run: (ctx, sessionId, signal) => ctx.sessionQuery.traceEvent({ sessionId, seq: 0 }, signal),
},
{
name: 'readEvent',
inspects: true,
run: (ctx, sessionId, signal) => ctx.sessionQuery.readEvent({ sessionId, seq: 0 }, signal),
},
] as const
describe.each(cancellableSessionListings)('$name cancellation', ({ run }) => {
it('preserves an exact pre-abort reason without entering persistence', async () => {
TestPersistence.reset()
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('session listing cancelled before start')
controller.abort(reason)
await expect(run(ctx, controller.signal)).rejects.toBe(reason)
expect(TestPersistence.listCalls).toBe(0)
expect(TestPersistence.listSignals).toEqual([])
})
it('forwards in-flight cancellation and waits for persistence cleanup before rejecting', async () => {
TestPersistence.reset()
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('session listing cancelled in flight')
const started = Promise.withResolvers<undefined>()
const abortObserved = Promise.withResolvers<undefined>()
const cleanup = Promise.withResolvers<undefined>()
let active = false
TestPersistence.listOverride = async (signal) => {
if (signal === undefined) throw new Error('expected persistence listing signal')
active = true
const aborted = new Promise<void>((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
started.resolve(undefined)
await aborted
abortObserved.resolve(undefined)
await cleanup.promise
active = false
signal.throwIfAborted()
return []
}
const pending = run(ctx, controller.signal)
let settled = false
void pending.then(
() => { settled = true },
() => { settled = true },
)
await started.promise
controller.abort(reason)
await abortObserved.promise
expect(settled).toBe(false)
expect(active).toBe(true)
expect(TestPersistence.listSignals).toEqual([controller.signal])
cleanup.resolve(undefined)
await expect(pending).rejects.toBe(reason)
expect(active).toBe(false)
})
it('preserves cancellation after a persistence implementation ignores the signal', async () => {
TestPersistence.reset()
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('session listing cancelled before persistence returned')
const started = Promise.withResolvers<undefined>()
const listing = Promise.withResolvers<SessionHeader[]>()
TestPersistence.listOverride = (_signal) => {
started.resolve(undefined)
return listing.promise
}
const pending = run(ctx, controller.signal)
await started.promise
controller.abort(reason)
listing.resolve([])
await expect(pending).rejects.toBe(reason)
expect(TestPersistence.listSignals).toEqual([controller.signal])
})
})
describe.each(cancellableExactReads)('$name cancellation', ({ inspects, run }) => {
it('preserves an exact pre-abort reason without entering persistence', async () => {
const persisted = header('pre-aborted-exact-read')
TestPersistence.reset([{ meta: persisted, events: eventLog() }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('exact read cancelled before start')
controller.abort(reason)
await expect(run(ctx, persisted.id, controller.signal)).rejects.toBe(reason)
expect(TestPersistence.listCalls).toBe(0)
expect(TestPersistence.inspectCalls).toEqual([])
})
it('forwards in-flight list cancellation and waits for cleanup before rejecting', async () => {
const persisted = header('cancelled-exact-list')
TestPersistence.reset([{ meta: persisted, events: eventLog() }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('exact read list cancelled in flight')
const started = Promise.withResolvers<undefined>()
const abortObserved = Promise.withResolvers<undefined>()
const cleanup = Promise.withResolvers<undefined>()
let active = false
TestPersistence.listOverride = async (signal) => {
if (signal === undefined) throw new Error('expected exact-read listing signal')
active = true
const aborted = new Promise<void>((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
started.resolve(undefined)
await aborted
abortObserved.resolve(undefined)
await cleanup.promise
active = false
signal.throwIfAborted()
return []
}
const pending = run(ctx, persisted.id, controller.signal)
let settled = false
void pending.then(
() => { settled = true },
() => { settled = true },
)
await started.promise
controller.abort(reason)
await abortObserved.promise
expect(settled).toBe(false)
expect(active).toBe(true)
expect(TestPersistence.listSignals).toEqual([controller.signal])
expect(TestPersistence.inspectCalls).toEqual([])
cleanup.resolve(undefined)
await expect(pending).rejects.toBe(reason)
expect(active).toBe(false)
})
it('waits for an ignoring backend to return before preserving the abort reason', async () => {
const persisted = header('ignored-exact-signal')
const entry = { meta: persisted, events: eventLog() }
TestPersistence.reset([entry])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('exact read cancelled while backend ignored signal')
const started = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
let active = false
if (inspects) {
TestPersistence.inspectOverride = async () => {
active = true
started.resolve(undefined)
await release.promise
active = false
return structuredClone(entry)
}
} else {
TestPersistence.listOverride = async () => {
active = true
started.resolve(undefined)
await release.promise
active = false
return [structuredClone(persisted)]
}
}
const pending = run(ctx, persisted.id, controller.signal)
let settled = false
void pending.then(
() => { settled = true },
() => { settled = true },
)
await started.promise
controller.abort(reason)
expect(settled).toBe(false)
expect(active).toBe(true)
expect(TestPersistence.listSignals).toEqual([controller.signal])
expect(TestPersistence.inspectSignals).toEqual(inspects ? [controller.signal] : [])
release.resolve(undefined)
await expect(pending).rejects.toBe(reason)
expect(active).toBe(false)
})
})
describe.each(cancellableExactReads.filter(read => read.inspects))(
'$name persisted inspection cancellation',
({ run }) => {
it('forwards cancellation and waits for inspection cleanup before rejecting', async () => {
const persisted = header('cancelled-exact-inspect')
TestPersistence.reset([{ meta: persisted, events: eventLog() }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('exact read inspection cancelled in flight')
const started = Promise.withResolvers<undefined>()
const abortObserved = Promise.withResolvers<undefined>()
const cleanup = Promise.withResolvers<undefined>()
let active = false
TestPersistence.inspectOverride = async (_sessionId, signal) => {
if (signal === undefined) throw new Error('expected exact-read inspection signal')
active = true
const aborted = new Promise<void>((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
started.resolve(undefined)
await aborted
abortObserved.resolve(undefined)
await cleanup.promise
active = false
signal.throwIfAborted()
throw new Error('unreachable after exact-read cancellation')
}
const pending = run(ctx, persisted.id, controller.signal)
let settled = false
void pending.then(
() => { settled = true },
() => { settled = true },
)
await started.promise
controller.abort(reason)
await abortObserved.promise
expect(settled).toBe(false)
expect(active).toBe(true)
expect(TestPersistence.listSignals).toEqual([controller.signal])
expect(TestPersistence.inspectSignals).toEqual([controller.signal])
cleanup.resolve(undefined)
await expect(pending).rejects.toBe(reason)
expect(active).toBe(false)
})
},
)
describe('session-query exact reads', () => {
it('returns a detached replay-valid full log and rejects a corrupt persisted seed', async () => {
const valid = header('valid-log', 2)
@@ -192,6 +500,341 @@ describe('session-query exact reads', () => {
expect(Object.keys((await ctx.sessionQuery.listSessions())[0]!)).toEqual(['header', 'live', 'persisted'])
})
it('batches unique persisted title observations through one cancellable corpus scan', async () => {
const first = header('batch-title-first', 1)
const second = header('batch-title-second', 2)
const titleEvent = (title: string, time: number): SessionEvent => ({
type: 'session/title',
seq: 0,
time,
data: {
title,
messageSeqs: [],
source: { kind: 'fallback' },
},
})
TestPersistence.reset([
{ meta: first, events: [titleEvent('First title', 10)] },
{ meta: second, events: [titleEvent('Second title', 20)] },
])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const signal = new AbortController().signal
const missing = SessionId('batch-title-missing')
const results = await ctx.sessionQuery.readTitleSnapshots(
[second.id, first.id, second.id, missing],
signal,
)
expect(results.map(result => [result.sessionId, result.status])).toEqual([
[second.id, 'fulfilled'],
[first.id, 'fulfilled'],
[missing, 'rejected'],
])
expect(results[0]).toMatchObject({ value: { session: second, title: { title: 'Second title' } } })
expect(results[1]).toMatchObject({ value: { session: first, title: { title: 'First title' } } })
expect(TestPersistence.listCalls).toBe(1)
expect(TestPersistence.inspectCalls).toEqual([second.id, first.id])
expect(TestPersistence.listSignals).toEqual([signal])
expect(TestPersistence.inspectSignals).toEqual([signal, signal])
})
it('bounds persisted title inspection concurrency while preserving ordered results', async () => {
const entries = Array.from({ length: 12 }, (_, index) => {
const meta = header(`bounded-title-${index}`, index)
return { meta, events: eventLog(`title-${index}`) }
})
TestPersistence.reset(entries)
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
let active = 0
let maximum = 0
TestPersistence.inspectOverride = async (id) => {
active += 1
maximum = Math.max(maximum, active)
await new Promise<void>(resolve => setImmediate(resolve))
active -= 1
const entry = TestPersistence.entries.get(id)
if (entry === undefined) throw new Error('missing bounded test session')
return structuredClone(entry)
}
const results = await ctx.sessionQuery.readTitleSnapshots(entries.map(entry => entry.meta.id))
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))
expect(results.every(result => result.status === 'fulfilled')).toBe(true)
})
it('folds and discards each completed log before its worker dequeues another inspection', async () => {
const entries = Array.from({ length: 5 }, (_, index) => ({
meta: header(`project-title-${index}`, index),
events: [],
}))
TestPersistence.reset(entries)
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const timeline: string[] = []
const releases = new Map<SessionIdType, () => void>()
TestPersistence.inspectOverride = id => new Promise((resolve) => {
timeline.push(`inspect:${id}`)
releases.set(id, () => {
const marker = `full-log-marker:${id}`
const titleEvent = {
type: 'session/title',
seq: 1,
time: 20,
data: {
title: `Projected ${id}`,
get messageSeqs() {
timeline.push(`project:${id}`)
return []
},
source: { kind: 'fallback' },
},
} as unknown as SessionEvent
resolve({
meta: entries.find(entry => entry.meta.id === id)!.meta,
events: [...eventLog(marker), titleEvent],
})
})
})
const release = (id: SessionIdType): void => {
const settle = releases.get(id)
if (settle === undefined) throw new Error(`inspection ${id} has not started`)
settle()
}
const ids = entries.map(entry => entry.meta.id)
const pending = ctx.sessionQuery.readTitleSnapshots(ids)
await vi.waitFor(() => { expect(TestPersistence.inspectCalls).toHaveLength(4) })
release(ids[0]!)
await vi.waitFor(() => { expect(TestPersistence.inspectCalls).toHaveLength(5) })
// Heap-retention assertions would depend on nondeterministic GC. This ordering
// is the deterministic guard: a retain-all implementation cannot touch the
// observable title getter until every inspection has completed.
expect(timeline.indexOf(`project:${ids[0]}`))
.toBeLessThan(timeline.indexOf(`inspect:${ids[4]}`))
for (const id of ids.slice(1)) release(id)
const results = await pending
expect(results.map(result => result.sessionId)).toEqual(ids)
expect(JSON.stringify(results)).not.toContain('full-log-marker:')
expect(results.every(result => result.status === 'fulfilled')).toBe(true)
})
it('passes cancellation into a stalled persisted title batch and rejects with its reason', async () => {
const persisted = header('stalled-title', 1)
TestPersistence.reset([{ meta: persisted, events: [] }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('title deadline')
let started!: () => void
const inspectStarted = new Promise<void>((resolve) => { started = resolve })
TestPersistence.inspectOverride = (_id, signal) => new Promise((_resolve, reject) => {
started()
signal?.addEventListener('abort', () => { reject(reason) }, { once: true })
})
const pending = ctx.sessionQuery.readTitleSnapshots([persisted.id], controller.signal)
await inspectStarted
controller.abort(reason)
await expect(pending).rejects.toBe(reason)
expect(TestPersistence.listSignals).toEqual([controller.signal])
expect(TestPersistence.inspectSignals).toEqual([controller.signal])
})
it('drains started title inspections after cancellation without starting queued ids', async () => {
const entries = Array.from({ length: 8 }, (_, index) => ({
meta: header(`cancel-queued-title-${index}`, index),
events: eventLog(`queued-${index}`),
}))
TestPersistence.reset(entries)
const persistedInspectConcurrency = 2
const ctx = await liveContext({ persistedInspectConcurrency })
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('cancel queued title batch')
const releases: Array<() => void> = []
let abortsObserved = 0
let inspectionsSettled = 0
TestPersistence.inspectOverride = (_id, signal) => new Promise((_resolve, reject) => {
signal?.addEventListener('abort', () => { abortsObserved += 1 }, { once: true })
releases.push(() => {
inspectionsSettled += 1
reject(reason)
})
})
const pending = ctx.sessionQuery.readTitleSnapshots(
entries.map(entry => entry.meta.id),
controller.signal,
)
let batchSettled = false
void pending.then(
() => { batchSettled = true },
() => { batchSettled = true },
)
await vi.waitFor(() => {
expect(TestPersistence.inspectCalls).toHaveLength(persistedInspectConcurrency)
})
controller.abort(reason)
await vi.waitFor(() => { expect(abortsObserved).toBe(persistedInspectConcurrency) })
expect(batchSettled).toBe(false)
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(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 () => {
const persisted = header('stalled-title-list', 1)
TestPersistence.reset([{ meta: persisted, events: [] }])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
const controller = new AbortController()
const reason = new Error('title listing deadline')
let started!: () => void
const listStarted = new Promise<void>((resolve) => { started = resolve })
TestPersistence.listOverride = signal => new Promise((_resolve, reject) => {
started()
signal?.addEventListener('abort', () => { reject(reason) }, { once: true })
})
const pending = ctx.sessionQuery.readTitleSnapshots([persisted.id], controller.signal)
await listStarted
controller.abort(reason)
await expect(pending).rejects.toBe(reason)
expect(TestPersistence.listSignals).toEqual([controller.signal])
expect(TestPersistence.inspectCalls).toEqual([])
})
it('isolates title read and fold failures while preferring a live owner attached during inspection', async () => {
const attached = header('batch-title-attached', 1)
const failed = header('batch-title-failed', 2)
const malformed = header('batch-title-malformed', 3)
const inspectFailure = new Error('one title inspect failed')
const malformedTitle = {
type: 'session/title',
seq: 0,
time: 30,
data: {
title: 'malformed',
source: { kind: 'fallback' },
},
} as unknown as SessionEvent
TestPersistence.reset([
{ meta: attached, events: eventLog('stale persisted') },
{ meta: failed, events: [] },
{ meta: malformed, events: [malformedTitle] },
])
const ctx = await liveContext()
await ctx.plugin(TestPersistence)
TestPersistence.inspectOverride = (id) => {
if (id === failed.id) return Promise.reject(inspectFailure)
const entry = TestPersistence.entries.get(id)
if (entry === undefined) return Promise.reject(new Error('missing test session'))
if (id === attached.id) {
const session = ctx.sessions.create(attached.id, { meta: { createdAt: attached.createdAt } })
session.append('session/title', {
title: 'Attached live title',
messageSeqs: [],
source: { kind: 'fallback' },
})
}
return Promise.resolve(structuredClone(entry))
}
const results = await ctx.sessionQuery.readTitleSnapshots([
attached.id,
failed.id,
malformed.id,
])
expect(results[0]).toMatchObject({
status: 'fulfilled',
value: { session: attached, title: { title: 'Attached live title' } },
})
expect(results[1]).toMatchObject({
sessionId: failed.id,
status: 'rejected',
reason: {
code: 'SESSION_QUERY_PERSISTENCE_FAILED',
cause: inspectFailure,
},
})
expect(results[2]).toMatchObject({ sessionId: malformed.id, status: 'rejected' })
if (results[2]?.status !== 'rejected') throw new Error('expected malformed title rejection')
expect(results[2].reason).toBeInstanceOf(TypeError)
})
it('preserves live batch results across missing persistence, listing failure, and late attachment', async () => {
const liveOnly = await liveContext()
const live = liveOnly.sessions.create(SessionId('batch-title-live'))
const missing = SessionId('batch-title-no-persistence')
await expect(liveOnly.sessionQuery.readTitleSnapshots([live.id, live.id])).resolves.toEqual([{
sessionId: live.id,
status: 'fulfilled',
value: { session: live.header },
}])
await expect(liveOnly.sessionQuery.readTitleSnapshots([live.id, missing])).resolves.toMatchObject([
{ sessionId: live.id, status: 'fulfilled' },
{ sessionId: missing, status: 'rejected' },
])
await expect(liveOnly.sessionQuery.readTitleSnapshot(missing))
.rejects.toThrow(expectCode('SESSION_QUERY_SESSION_NOT_FOUND'))
const persisted = header('batch-title-persisted', 1)
const late = header('batch-title-late', 2)
TestPersistence.reset([{ meta: persisted, events: [] }])
const mixed = await liveContext()
const mixedLive = mixed.sessions.create(SessionId('batch-title-mixed-live'))
await mixed.plugin(TestPersistence)
TestPersistence.afterList = () => {
mixed.sessions.create(late.id, { meta: { createdAt: late.createdAt } })
TestPersistence.afterList = undefined
}
await expect(mixed.sessionQuery.readTitleSnapshots([
mixedLive.id,
persisted.id,
late.id,
])).resolves.toMatchObject([
{ sessionId: mixedLive.id, status: 'fulfilled' },
{ sessionId: persisted.id, status: 'fulfilled' },
{ sessionId: late.id, status: 'fulfilled' },
])
TestPersistence.reset()
TestPersistence.listFailure = new Error('title listing failed')
const failedList = await liveContext()
const survivingLive = failedList.sessions.create(SessionId('batch-title-list-live'))
await failedList.plugin(TestPersistence)
await expect(failedList.sessionQuery.readTitleSnapshots([survivingLive.id, missing]))
.resolves.toMatchObject([
{ sessionId: survivingLive.id, status: 'fulfilled' },
{
sessionId: missing,
status: 'rejected',
reason: expectCode('SESSION_QUERY_PERSISTENCE_FAILED'),
},
])
})
it('lists live sessions deterministically and returns detached headers', async () => {
const ctx = await liveContext()
const older = ctx.sessions.create(SessionId('older'), { meta: { createdAt: 1 } })
@@ -408,9 +1051,15 @@ describe('session-query exact reads', () => {
await ctx.plugin(TestPersistence)
TestPersistence.listFailure = new Error('list unavailable')
TestPersistence.inspectFailure = new Error('inspect unavailable')
const signal = new AbortController().signal
await expect(ctx.sessionQuery.listEvents(live.id)).resolves.toHaveLength(2)
await expect(ctx.sessionQuery.readEvent({ sessionId: live.id, seq: 1 })).resolves.toMatchObject({ target: { seq: 1 } })
await expect(ctx.sessionQuery.traceEvent({ sessionId: live.id, seq: 1 }, signal))
.resolves.toMatchObject({ session: { id: live.id }, target: { seq: 1 } })
await expect(ctx.sessionQuery.readEvent({ sessionId: live.id, seq: 1 }, signal))
.resolves.toMatchObject({ target: { seq: 1 } })
expect(TestPersistence.listSignals).toEqual([])
expect(TestPersistence.inspectSignals).toEqual([])
await expect(ctx.sessionQuery.listSessions()).rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
await expect(ctx.sessionQuery.listEvents(SessionId('durable'))).rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
})
@@ -459,10 +1108,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 () => {

View File

@@ -1,6 +1,6 @@
import SessionQueryService from '@deepseek-ai/dsh-session-query'
import type {
SessionEventSearchHit,
SessionEventSearchPage,
SessionEventSearchRequest,
SessionSearchExecContext,
SessionSearchHit,
@@ -17,10 +17,13 @@ export class TestSessionQueryService extends SessionQueryService {
return Promise.resolve({ items: [] })
}
override searchEvents(
_request: SessionEventSearchRequest,
override async searchEvents(
request: SessionEventSearchRequest,
_exec?: SessionSearchExecContext,
): Promise<SessionSearchPage<SessionEventSearchHit>> {
return Promise.resolve({ items: [] })
): Promise<SessionEventSearchPage> {
return {
session: (await this.readSurface(request.sessionId)).session,
items: [],
}
}
}

View File

@@ -0,0 +1,74 @@
# @deepseek-ai/dsh-tool-session-query
Workspace-authorized model tools over `ctx.sessionQuery`. The opt-in package depends only on the unified interface and registers `session_search`, `session_event_search`, `session_trace`, `session_event_trace`, and `session_event_read`; shipped host compositions do not mount it by default.
## Configuration
| Key | Default | Meaning |
|---|---:|---|
| `maxSearchResults` | `100` | Maximum authorized non-self hits collected across internal provider pages |
| `searchTimeoutMs` | `30000` | Cooperative deadline attached to both full-text search tools |
The caller comes exclusively from `ToolExecution.exec.agent`. Cross-session access requires exact equality between the target and caller session `cwd` values; a caller without `cwd` can inspect only itself. Search never exposes provider cursors, offsets, page sizes, or a model-controlled limit. Because one search consumes generation-bound provider cursors internally, both search tools execute exclusively with sibling tool calls; the three exact trace/read tools opt into parallel execution. Every exact executor passes its unchanged execution signal through authorization and the service trace/read, so cancellation waits for cooperative persistence cleanup and retains the signal's exact reason. Timestamps at the tool boundary require an explicit `Z` or numeric offset and become inclusive epoch-millisecond filters.
`session_search` always omits the caller session. Requested parent ids are deduplicated and checked against caller-workspace authority before FTS; only authorized ids reach the provider, while missing and cross-workspace guesses behave identically and the root marker remains independently ORed. A current-session `session_event_search` stops immediately before the step that invoked it, so the active assistant output and logged tool call cannot match themselves. Direct targets are authorized before trace, event, or title reads. Lineage output replaces unauthorized ancestor and descendant boundaries with markers that contain no hidden session id.
Every trusted `ctx.sessionQuery` call crosses one model-boundary sanitizer. Caller cancellation is checked first and preserved exactly. Available corpus and provider diagnostics, including safely inspectable nested causes, are logged internally on a best-effort basis; unprintable failures use a fixed log placeholder. Diagnostic formatting and error classification are independently guarded, so an unprintable cause cannot escape or prevent a safely classified outer error, while unsafe classification or logging falls back to the fixed `SESSION_QUERY_TOOL_FAILED` code and message. Local argument-validation and authorization errors retain their precise tool-owned messages.
The package deliberately performs no byte or character truncation and does not import a spill backend. Deployments that need bounded inline output mount `@deepseek-ai/dsh-spill-policy`, which can replace the rendered text after execution while retaining the complete result.
## Model Experience
### System prompt
#### What the model sees
The model receives one fixed prior-history guidance section.
##### Prior-history guidance
```markdown
Use session_search to find relevant work from prior sessions, or session_event_search to search earlier events in one session. Search results are cursor-free and workspace-scoped. Follow a useful hit with session_trace, session_event_trace, or session_event_read when you need lineage, relationships, or exact data.
```
#### Token effect
One fixed concise section is present on each request while the plugin is mounted.
#### KV Cache effect
Prefix-stable while the plugin and guidance text are unchanged.
### Tool schemas
#### What the model sees
The model sees the generated [`session_search`, `session_event_search`, `session_trace`, `session_event_trace`, and `session_event_read` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-session-query). Search filters add fixed schema tokens, while cursors, workspace paths, output pagination, and model-controlled result limits remain absent.
#### Token effect
Five fixed read-only schemas are sent on each request while visible.
#### KV Cache effect
Prefix-stable while tool visibility and definitions are unchanged.
### Tool results
#### What the model sees
Each successful call emits one plain-text block. Search results include titles and best-match excerpts; traces include all authorized relationships; event reads include unabridged target JSON. The generic spill policy may replace oversized inline text with its preview, opaque locator, and retrieval hint.
#### Token effect
Results are data-dependent and remain in logged tool history until compaction; `maxSearchResults` bounds search-hit count.
#### KV Cache effect
Append-only result text follows the reusable request prefix and does not invalidate earlier cache entries.
## Known Limitations and Deferred Work
- Search returns at most the deployment cap and asks the model to narrow its query when more matches exist; it offers no continuation token.
- Workspace identity is conservative exact-string `cwd` equality, so symlink-equivalent paths do not share authority.
- Custom compositions without the generic spill policy accept complete trace and event payloads inline.

View File

@@ -0,0 +1,58 @@
{
"name": "@deepseek-ai/dsh-tool-session-query",
"description": "Workspace-authorized model-facing session history search, trace, and event read tools",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-query": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-timeout": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
"@deepseek-ai/dsh-session-title": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -0,0 +1,137 @@
/**
* Model-facing, workspace-authorized session-history search and read tools.
*
* @module @deepseek-ai/dsh-tool-session-query
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-system-prompt'
import { toolInput } from './input.ts'
import { operations } from './operations.ts'
import { presentation } from './presentation.ts'
/** Cordis plugin name used by Loader diagnostics. */
export const name = 'tool-session-query'
/** Capability services required by the model-facing consumer. */
export const inject = ['tools', 'systemPrompt', 'sessionQuery']
/** Default maximum number of authorized search hits returned by one call. */
export const DEFAULT_MAX_SEARCH_RESULTS = 100
/** Default cooperative deadline for either full-text search tool. */
export const DEFAULT_SEARCH_TIMEOUT_MS = 30_000
/** Deployment-owned search count and timeout bounds. */
export interface Config {
/** Maximum authorized hits returned by one search call. Defaults to 100. */
maxSearchResults?: number
/** Cooperative full-text search deadline in milliseconds. Defaults to 30000. */
searchTimeoutMs?: number
}
/** Schemastery config for Loader defaults and generated configuration docs. */
export const Config: z<Config> = z.object({
maxSearchResults: z.number().step(1).min(1).default(DEFAULT_MAX_SEARCH_RESULTS),
searchTimeoutMs: z.number().step(1).min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_SEARCH_TIMEOUT_MS),
})
interface ResolvedConfig {
readonly maxSearchResults: number
readonly searchTimeoutMs: number
}
const TEXT_OUTPUT = {
schema: { type: 'string' as const },
render: (_args: unknown, value: string) => [{ type: 'text' as const, text: value }],
}
const PROMPT_TEXT =
'Use session_search to find relevant work from prior sessions, or session_event_search to search earlier '
+ 'events in one session. Search results are cursor-free and workspace-scoped. Follow a useful hit with '
+ 'session_trace, session_event_trace, or session_event_read when you need lineage, relationships, or exact data.'
/** Register all five tools and their shared model guidance. */
export function apply(ctx: Context, config: Config): void {
const resolved = resolveConfig(config)
ctx.systemPrompt.section({
name: 'tool:session-query',
order: 113,
text: PROMPT_TEXT,
})
ctx.tools.register(defineTool({
name: 'session_search',
description: 'Search prior sessions in the caller workspace and return the strongest matching event from each session.',
parameters: toolInput.sessionSearchParameters,
output: TEXT_OUTPUT,
timeoutMs: resolved.searchTimeoutMs,
execute: (args, exec) => operations.executeSessionSearch(ctx, args, exec, resolved.maxSearchResults),
presentCall: presentation.presentSessionSearchCall,
}))
ctx.tools.register(defineTool({
name: 'session_event_search',
description: 'Search prior events in one authorized session; the current session excludes the step performing this call.',
parameters: toolInput.eventSearchParameters,
output: TEXT_OUTPUT,
timeoutMs: resolved.searchTimeoutMs,
execute: (args, exec) => operations.executeEventSearch(ctx, args, exec, resolved.maxSearchResults),
presentCall: presentation.presentEventSearchCall,
}))
ctx.tools.register(defineTool({
name: 'session_trace',
description: 'Read the authorized session lineage around one session, including complete visible ancestor and descendant relationships.',
parameters: toolInput.targetSessionParameter,
output: TEXT_OUTPUT,
isConcurrencySafe: () => true,
execute: (args, exec) => operations.executeSessionTrace(ctx, args, exec),
presentCall: presentation.presentSessionTraceCall,
}))
ctx.tools.register(defineTool({
name: 'session_event_trace',
description: 'Read every direct replacement and provenance relationship for one event in an authorized session.',
parameters: {
...toolInput.targetSessionParameter,
seq: { type: 'integer', required: true, description: 'Target event sequence number.' },
},
output: TEXT_OUTPUT,
isConcurrencySafe: () => true,
execute: (args, exec) => operations.executeEventTrace(ctx, args, exec),
presentCall: args => presentation.presentEventTargetCall('Trace event', args),
}))
ctx.tools.register(defineTool({
name: 'session_event_read',
description: 'Read one full unabridged event and optional neighboring raw-event summaries from an authorized session.',
parameters: {
...toolInput.targetSessionParameter,
seq: { type: 'integer', required: true, description: 'Target event sequence number.' },
before: { type: 'integer', description: 'Number of preceding raw events to summarize. Omit for none.' },
after: { type: 'integer', description: 'Number of following raw events to summarize. Omit for none.' },
},
output: TEXT_OUTPUT,
isConcurrencySafe: () => true,
execute: (args, exec) => operations.executeEventRead(ctx, args, exec),
presentCall: args => presentation.presentEventTargetCall('Read event', args),
}))
}
function resolveConfig(config: Config): ResolvedConfig {
const maxSearchResults = config.maxSearchResults ?? DEFAULT_MAX_SEARCH_RESULTS
const searchTimeoutMs = config.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS
if (!Number.isSafeInteger(maxSearchResults) || maxSearchResults < 1) {
throw new TypeError('tool-session-query: maxSearchResults must be a positive safe integer')
}
if (!Number.isInteger(searchTimeoutMs) || searchTimeoutMs < 1 || searchTimeoutMs > MAX_TIMER_DELAY_MS) {
throw new TypeError(
`tool-session-query: searchTimeoutMs must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`,
)
}
return { maxSearchResults, searchTimeoutMs }
}

View File

@@ -0,0 +1,307 @@
/**
* Model argument schemas, normalization, and filter construction.
*
* @module @deepseek-ai/dsh-tool-session-query/input
*/
import {
SessionId,
type SessionEventType,
type SessionId as SessionIdValue,
} from '@deepseek-ai/dsh-session'
import {
SessionQueryError,
type SessionAvailability,
type SessionEventMetadataFilter,
type SessionEventSurface,
type SessionResultFilter,
} from '@deepseek-ai/dsh-session-query'
interface SessionSearchArgs {
query: string
session_ids?: string[]
created_at_from?: string
created_at_to?: string
parent_session_ids?: string[]
include_root_sessions?: boolean
availability?: SessionAvailability[]
event_seq_from?: number
event_seq_to?: number
event_time_from?: string
event_time_to?: string
event_types?: string[]
event_surfaces?: SessionEventSurface[]
}
interface EventFilterInput {
readonly seqFrom?: number | undefined
readonly seqTo?: number | undefined
readonly timeFrom?: string | undefined
readonly timeTo?: string | undefined
readonly eventTypes?: string[] | undefined
readonly surfaces?: SessionEventSurface[] | undefined
}
const sessionSearchParameters = {
query: { type: 'string', required: true, description: 'Literal full-text query over prior session history.' },
session_ids: { type: 'array', items: { type: 'string' }, description: 'Optional session ids to include.' },
created_at_from: { type: 'string', description: 'Inclusive timezone-qualified ISO 8601 creation-time lower bound.' },
created_at_to: { type: 'string', description: 'Inclusive timezone-qualified ISO 8601 creation-time upper bound.' },
parent_session_ids: { type: 'array', items: { type: 'string' }, description: 'Optional direct parent session ids.' },
include_root_sessions: { type: 'boolean', description: 'Include sessions with no parent in the parent filter.' },
availability: {
type: 'array',
items: { type: 'string', enum: ['live', 'persisted'] },
description: 'Require at least one selected source availability.',
},
event_seq_from: { type: 'integer', description: 'Inclusive event sequence lower bound.' },
event_seq_to: { type: 'integer', description: 'Inclusive event sequence upper bound.' },
event_time_from: { type: 'string', description: 'Inclusive timezone-qualified ISO 8601 event-time lower bound.' },
event_time_to: { type: 'string', description: 'Inclusive timezone-qualified ISO 8601 event-time upper bound.' },
event_types: { type: 'array', items: { type: 'string' }, description: 'Event types to include.' },
event_surfaces: {
type: 'array',
items: { type: 'string', enum: ['current', 'shadowed', 'log-only'] },
description: 'Event surfaces to include.',
},
} as const
const eventSearchParameters = {
session_id: { type: 'string', description: 'Target session id. Omit for the current session.' },
query: { type: 'string', required: true, description: 'Literal full-text query over the target session.' },
seq_from: { type: 'integer', description: 'Inclusive event sequence lower bound.' },
seq_to: { type: 'integer', description: 'Inclusive event sequence upper bound.' },
time_from: { type: 'string', description: 'Inclusive timezone-qualified ISO 8601 event-time lower bound.' },
time_to: { type: 'string', description: 'Inclusive timezone-qualified ISO 8601 event-time upper bound.' },
event_types: { type: 'array', items: { type: 'string' }, description: 'Event types to include.' },
surfaces: {
type: 'array',
items: { type: 'string', enum: ['current', 'shadowed', 'log-only'] },
description: 'Event surfaces to include.',
},
} as const
const targetSessionParameter = {
session_id: { type: 'string', description: 'Target session id. Omit for the current session.' },
} as const
function buildSessionFilters(args: SessionSearchArgs): SessionResultFilter[] {
const filters: SessionResultFilter[] = []
if (args.session_ids !== undefined) {
assertNonEmptyArray('session_ids', args.session_ids)
filters.push({ kind: 'id', values: args.session_ids.map(SessionId) })
}
const created = timestampRange('created_at', args.created_at_from, args.created_at_to)
if (created !== undefined) filters.push({ kind: 'created-at', ...created })
if (args.availability !== undefined) {
assertNonEmptyArray('availability', args.availability)
filters.push({ kind: 'availability', values: args.availability })
}
return filters
}
function materializeParentSessionIds(values: readonly string[] | undefined): SessionIdValue[] | undefined {
if (values === undefined) return undefined
assertNonEmptyArray('parent_session_ids', values)
return [...new Set(values.map(SessionId))]
}
function buildEventFilters(input: EventFilterInput): SessionEventMetadataFilter[] {
const filters: SessionEventMetadataFilter[] = []
const seq = sequenceRange(input.seqFrom, input.seqTo)
if (seq.from !== undefined || seq.to !== undefined) filters.push({ kind: 'seq', ...seq })
const time = timestampRange('time', input.timeFrom, input.timeTo)
if (time !== undefined) filters.push({ kind: 'time', ...time })
if (input.eventTypes !== undefined) {
assertNonEmptyArray('event_types', input.eventTypes)
filters.push({ kind: 'type', values: input.eventTypes as SessionEventType[] })
}
if (input.surfaces !== undefined) {
assertNonEmptyArray('surfaces', input.surfaces)
filters.push({ kind: 'surface', values: input.surfaces })
}
return filters
}
function normalizeQuery(value: string): string {
const query = value.trim().replace(/\s+/gu, ' ')
if (query.length === 0) {
throw new SessionQueryError(
'session-search query must contain non-whitespace text',
'SESSION_QUERY_INVALID_QUERY',
)
}
if (query.includes('\0')) {
throw new SessionQueryError(
'session-search query must not contain NUL',
'SESSION_QUERY_INVALID_QUERY',
)
}
return query
}
function sequenceRange(
from: number | undefined,
to: number | undefined,
): { from?: number; to?: number } {
if (from !== undefined) assertNonNegativeSafeInteger('sequence lower bound', from)
if (to !== undefined) assertNonNegativeSafeInteger('sequence upper bound', to)
if (from !== undefined && to !== undefined && from > to) {
throw invalidRange('sequence', 'from must be less than or equal to to')
}
return {
...from === undefined ? {} : { from },
...to === undefined ? {} : { to },
}
}
function timestampRange(
name: string,
from: string | undefined,
to: string | undefined,
): { from?: number; to?: number } | undefined {
if (from === undefined && to === undefined) return undefined
const fromTimestamp = from === undefined ? undefined : parseIsoTimestamp(`${name}_from`, from)
const toTimestamp = to === undefined ? undefined : parseIsoTimestamp(`${name}_to`, to)
if (
fromTimestamp !== undefined
&& toTimestamp !== undefined
&& compareTimestamps(fromTimestamp, toTimestamp) > 0
) {
throw invalidRange(name, 'from must be less than or equal to to')
}
return {
...fromTimestamp === undefined ? {} : { from: timestampLowerBound(fromTimestamp) },
...toTimestamp === undefined ? {} : { to: timestampUpperBound(toTimestamp) },
}
}
const ISO_TIMESTAMP =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|([+-])(\d{2}):(\d{2}))$/
interface ExactTimestamp {
readonly millisecond: number
/** Canonical decimal digits strictly below one millisecond; no trailing zeroes. */
readonly remainder: string
}
function parseIsoTimestamp(name: string, value: string): ExactTimestamp {
const match = ISO_TIMESTAMP.exec(value)
if (match === null) {
throw invalidRange(name, 'must be an ISO 8601 timestamp with Z or a numeric offset')
}
const year = Number(match[1])
const month = Number(match[2])
const day = Number(match[3])
const hour = Number(match[4])
const minute = Number(match[5])
const second = Number(match[6] ?? 0)
const offsetHour = Number(match[10] ?? 0)
const offsetMinute = Number(match[11] ?? 0)
if (
month < 1 || month > 12
|| day < 1 || day > daysInMonth(year, month)
|| hour > 23 || minute > 59 || second > 59
|| offsetHour > 23 || offsetMinute > 59
) {
throw invalidRange(name, 'must be a valid ISO 8601 timestamp')
}
const fraction = match[7] ?? ''
const millisecondDigits = fraction.slice(0, 3).padEnd(3, '0')
const normalized = `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`
+ `:${match[6] ?? '00'}.${millisecondDigits}${match[8]}`
const timestamp = Date.parse(normalized)
if (!Number.isSafeInteger(timestamp)) {
throw invalidRange(name, 'must be a valid ISO 8601 timestamp')
}
return {
millisecond: timestamp,
remainder: fraction.slice(3).replace(/0+$/u, ''),
}
}
function compareTimestamps(left: ExactTimestamp, right: ExactTimestamp): number {
if (left.millisecond !== right.millisecond) {
return left.millisecond < right.millisecond ? -1 : 1
}
const length = Math.max(left.remainder.length, right.remainder.length)
for (let index = 0; index < length; index += 1) {
const leftDigit = left.remainder[index] ?? '0'
const rightDigit = right.remainder[index] ?? '0'
if (leftDigit !== rightDigit) return leftDigit < rightDigit ? -1 : 1
}
return 0
}
function timestampLowerBound(timestamp: ExactTimestamp): number {
return timestamp.remainder.length === 0
? timestamp.millisecond
: nextUpFinite(timestamp.millisecond)
}
function timestampUpperBound(timestamp: ExactTimestamp): number {
return timestamp.remainder.length === 0
? timestamp.millisecond
: nextDownFinite(timestamp.millisecond + 1)
}
function nextUpFinite(value: number): number {
if (value === 0) return Number.MIN_VALUE
const view = new DataView(new ArrayBuffer(8))
view.setFloat64(0, value)
const bits = view.getBigUint64(0)
view.setBigUint64(0, value > 0 ? bits + 1n : bits - 1n)
return view.getFloat64(0)
}
function nextDownFinite(value: number): number {
if (value === 0) return -Number.MIN_VALUE
const view = new DataView(new ArrayBuffer(8))
view.setFloat64(0, value)
const bits = view.getBigUint64(0)
view.setBigUint64(0, value > 0 ? bits - 1n : bits + 1n)
return view.getFloat64(0)
}
function daysInMonth(year: number, month: number): number {
if (month === 2) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28
return [4, 6, 9, 11].includes(month) ? 30 : 31
}
function invalidRange(name: string, detail: string): SessionQueryError {
return new SessionQueryError(
`session ${name} range ${detail}`,
'SESSION_QUERY_INVALID_FILTER',
)
}
function assertNonNegativeSafeInteger(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new SessionQueryError(
`${name} must be a non-negative safe integer`,
'SESSION_QUERY_INVALID_FILTER',
)
}
}
function assertNonEmptyArray(name: string, values: readonly unknown[]): void {
if (values.length === 0) {
throw new SessionQueryError(
`${name} must contain at least one value when supplied`,
'SESSION_QUERY_INVALID_FILTER',
)
}
}
/** Model schemas and model-owned value normalization shared by tool operations. */
export const toolInput = {
sessionSearchParameters,
eventSearchParameters,
targetSessionParameter,
buildSessionFilters,
materializeParentSessionIds,
buildEventFilters,
normalizeQuery,
sequenceRange,
assertNonNegativeSafeInteger,
}

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-session-query`.
* @module @deepseek-ai/dsh-tool-session-query/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-session-query'
/** Cordis companion plugin name. */
export const name = 'tool-session-query-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this read-only model adapter owns no event or mutable
* data relationship beyond the registries that already validate registration.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -0,0 +1,281 @@
/**
* Tool operation orchestration over session-query service capabilities.
*
* @module @deepseek-ai/dsh-tool-session-query/operations
*/
import type { Context } from 'cordis'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import type { SessionId } from '@deepseek-ai/dsh-session'
import {
SessionQueryError,
type SessionEventSearchPage,
type SessionEventSurface,
type SessionRecord,
type SessionSearchCursor,
} from '@deepseek-ai/dsh-session-query'
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
import { toolInput } from './input.ts'
import { presentation } from './presentation.ts'
import { serviceBoundary } from './service-boundary.ts'
import { workspaceAccess } from './workspace-access.ts'
type SessionSearchArgs = Parameters<typeof toolInput.buildSessionFilters>[0]
interface EventSearchArgs {
session_id?: string
query: string
seq_from?: number
seq_to?: number
time_from?: string
time_to?: string
event_types?: string[]
surfaces?: SessionEventSurface[]
}
interface SessionTargetArgs {
session_id?: string
}
interface EventTargetArgs extends SessionTargetArgs {
seq: number
}
interface EventReadArgs extends EventTargetArgs {
before?: number
after?: number
}
interface SearchCollection<T> {
readonly items: T[]
readonly capped: boolean
}
async function executeSessionSearch(
ctx: Context,
args: SessionSearchArgs,
exec: ToolRunContext,
maxResults: number,
): Promise<string> {
const caller = workspaceAccess.callerOf(exec)
const cwd = caller.header.cwd
if (cwd === undefined) {
throw new HarnessError(
'cross-session search is unavailable because the caller session has no workspace',
'SESSION_QUERY_TOOL_UNAUTHORIZED',
)
}
const query = toolInput.normalizeQuery(args.query)
const sessionFilters = toolInput.buildSessionFilters(args)
const eventFilters = toolInput.buildEventFilters({
seqFrom: args.event_seq_from,
seqTo: args.event_seq_to,
timeFrom: args.event_time_from,
timeTo: args.event_time_to,
eventTypes: args.event_types,
surfaces: args.event_surfaces,
})
const requestedParentIds = toolInput.materializeParentSessionIds(args.parent_session_ids)
if (requestedParentIds !== undefined || args.include_root_sessions === true) {
const authorizedParentIds = requestedParentIds === undefined
? new Set<SessionId>()
: await workspaceAccess.authorizeSessionIds(ctx, caller, requestedParentIds, exec.signal)
const parentValues: Array<SessionId | null> = requestedParentIds
?.filter(id => authorizedParentIds.has(id)) ?? []
if (args.include_root_sessions === true) parentValues.push(null)
if (parentValues.length === 0) return presentation.formatEmptySessionSearch()
sessionFilters.push({ kind: 'parent', values: parentValues })
}
sessionFilters.push({ kind: 'cwd', values: [cwd] })
const collected = await collectPages(
maxResults,
exec.signal,
cursor => serviceBoundary.call(ctx, exec.signal, 'session search', () =>
ctx.sessionQuery.searchSessions({
query,
sessionFilters,
eventFilters,
...cursor === undefined ? {} : { cursor },
}, { signal: exec.signal })),
hit => hit.header.id !== caller.id && workspaceAccess.recordAuthorized(hit, caller),
)
const parentIds = collected.items
.map(hit => hit.header.parentSession)
.filter((id): id is SessionId => id !== undefined)
const authorizedParents = await workspaceAccess.authorizeSessionIds(ctx, caller, parentIds, exec.signal)
const titles = await workspaceAccess.readTitles(
ctx,
caller,
collected.items.map(hit => hit.header.id),
exec.signal,
)
return presentation.formatSessionSearch(collected, titles, authorizedParents)
}
async function executeEventSearch(
ctx: Context,
args: EventSearchArgs,
exec: ToolRunContext,
maxResults: number,
): Promise<string> {
const caller = workspaceAccess.callerOf(exec)
const sessionId = workspaceAccess.targetId(args, caller)
await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal)
const query = toolInput.normalizeQuery(args.query)
const range = toolInput.sequenceRange(args.seq_from, args.seq_to)
if (sessionId === caller.id) {
const stepStart = caller.events.findLast(event => event.type === 'step/start')
if (stepStart === undefined) {
throw new HarnessError(
'current-session search requires an active step boundary',
'SESSION_QUERY_TOOL_NO_CURRENT_STEP',
)
}
range.to = Math.min(range.to ?? Number.MAX_SAFE_INTEGER, stepStart.seq - 1)
}
const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal)
if (range.from !== undefined && range.to !== undefined && range.from > range.to) {
return presentation.formatEventSearch(sessionId, title, { items: [], capped: false })
}
const filters = toolInput.buildEventFilters({
seqFrom: range.from,
seqTo: range.to,
timeFrom: args.time_from,
timeTo: args.time_to,
eventTypes: args.event_types,
surfaces: args.surfaces,
})
const collected = await collectPages(
maxResults,
exec.signal,
async (cursor): Promise<SessionEventSearchPage> => {
const page = await serviceBoundary.call(ctx, exec.signal, 'event search', () =>
ctx.sessionQuery.searchEvents({
sessionId,
query,
filters,
...cursor === undefined ? {} : { cursor },
}, { signal: exec.signal }))
workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, page.session)
return page
},
() => true,
)
return presentation.formatEventSearch(sessionId, title, collected)
}
async function executeSessionTrace(
ctx: Context,
args: SessionTargetArgs,
exec: ToolRunContext,
): Promise<string> {
const caller = workspaceAccess.callerOf(exec)
const sessionId = workspaceAccess.targetId(args, caller)
await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal)
const trace = await serviceBoundary.call(ctx, exec.signal, 'session lineage trace', () =>
ctx.sessionQuery.traceSession(sessionId, exec.signal))
workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.target.header)
const ancestors: SessionRecord[] = []
let ancestorBoundary = false
for (const ancestor of trace.ancestors) {
if (!workspaceAccess.recordAuthorized(ancestor, caller)) {
ancestorBoundary = true
break
}
ancestors.push(ancestor)
}
if (ancestors.length === trace.ancestors.length && !trace.complete) ancestorBoundary = true
const descendants = workspaceAccess.authorizeDescendants(trace.descendants, caller)
const visibleIds = [
trace.target.header.id,
...ancestors.map(record => record.header.id),
...workspaceAccess.descendantIds(descendants),
]
const titles = await workspaceAccess.readTitles(ctx, caller, visibleIds, exec.signal)
return presentation.formatSessionTrace(trace, ancestors, ancestorBoundary, descendants, titles)
}
async function executeEventTrace(
ctx: Context,
args: EventTargetArgs,
exec: ToolRunContext,
): Promise<string> {
toolInput.assertNonNegativeSafeInteger('seq', args.seq)
const caller = workspaceAccess.callerOf(exec)
const sessionId = workspaceAccess.targetId(args, caller)
await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal)
const trace = await serviceBoundary.call(ctx, exec.signal, 'event trace', () =>
ctx.sessionQuery.traceEvent({ sessionId, seq: args.seq }, exec.signal))
workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.session)
const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal)
return presentation.formatEventTrace(sessionId, title, trace)
}
async function executeEventRead(
ctx: Context,
args: EventReadArgs,
exec: ToolRunContext,
): Promise<string> {
toolInput.assertNonNegativeSafeInteger('seq', args.seq)
if (args.before !== undefined) toolInput.assertNonNegativeSafeInteger('before', args.before)
if (args.after !== undefined) toolInput.assertNonNegativeSafeInteger('after', args.after)
const caller = workspaceAccess.callerOf(exec)
const sessionId = workspaceAccess.targetId(args, caller)
await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal)
const window = await serviceBoundary.call(ctx, exec.signal, 'event read', () =>
ctx.sessionQuery.readEvent({
sessionId,
seq: args.seq,
...args.before === undefined ? {} : { before: args.before },
...args.after === undefined ? {} : { after: args.after },
}, exec.signal))
workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, window.session)
const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal)
return presentation.formatEventRead(sessionId, title, window)
}
async function collectPages<T>(
maxResults: number,
signal: AbortSignal,
request: (cursor?: SessionSearchCursor) => Promise<{
readonly items: readonly T[]
readonly nextCursor?: SessionSearchCursor
}>,
accept: (item: T) => boolean,
): Promise<SearchCollection<T>> {
const items: T[] = []
const seen = new Set<SessionSearchCursor>()
let cursor: SessionSearchCursor | undefined
while (true) {
signal.throwIfAborted()
const page = await request(cursor)
signal.throwIfAborted()
for (const item of page.items) {
if (!accept(item)) continue
if (items.length === maxResults) {
return { items, capped: true }
}
items.push(item)
}
if (page.nextCursor === undefined) return { items, capped: false }
if (seen.has(page.nextCursor)) {
throw new SessionQueryError(
'session-search provider repeated a continuation cursor',
'SESSION_QUERY_INVALID_CURSOR',
)
}
seen.add(page.nextCursor)
cursor = page.nextCursor
}
}
/** Five model-facing session-query operation implementations. */
export const operations = {
executeSessionSearch,
executeEventSearch,
executeSessionTrace,
executeEventTrace,
executeEventRead,
}

View File

@@ -0,0 +1,255 @@
/**
* Model text rendering and generic tool-call presentation.
*
* @module @deepseek-ai/dsh-tool-session-query/presentation
*/
import {
extractSessionEventText,
type SessionEventSearchHit,
type SessionEventTraceObservation,
type SessionEventWindow,
type SessionLineageTrace,
type SessionRecord,
type SessionSearchHit,
} from '@deepseek-ai/dsh-session-query'
import type {
SessionEvent,
SessionId,
} from '@deepseek-ai/dsh-session'
import type { GenericCallView } from '@deepseek-ai/dsh-tools'
import { workspaceAccess } from './workspace-access.ts'
type TitleView = Awaited<ReturnType<typeof workspaceAccess.readTitle>>
type CompleteTitleMap = Awaited<ReturnType<typeof workspaceAccess.readTitles>>
type AuthorizedDescendants = ReturnType<typeof workspaceAccess.authorizeDescendants>
interface SearchCollection<T> {
readonly items: T[]
readonly capped: boolean
}
interface SessionSearchCallArgs {
readonly query: string
}
interface EventSearchCallArgs {
readonly query: string
}
interface SessionTargetCallArgs {
readonly session_id?: string
}
interface EventTargetCallArgs extends SessionTargetCallArgs {
readonly seq: number
}
function formatSessionSearch(
collected: SearchCollection<SessionSearchHit>,
titles: CompleteTitleMap,
authorizedParents: ReadonlySet<SessionId>,
): string {
if (collected.items.length === 0) return formatEmptySessionSearch()
const lines = [`Session search results (${collected.items.length}):`]
for (const [index, hit] of collected.items.entries()) {
const parent = hit.header.parentSession === undefined
? 'root'
: authorizedParents.has(hit.header.parentSession)
? hit.header.parentSession
: '[outside workspace]'
const availability = [
hit.live ? 'live' : undefined,
hit.persisted ? 'persisted' : undefined,
].filter((value): value is string => value !== undefined).join(', ') || 'unavailable'
lines.push(
'',
`${index + 1}. Session ${hit.header.id} — ${workspaceAccess.titleText(titles.get(hit.header.id))}`,
` Created: ${formatTime(hit.header.createdAt)}`,
` Parent: ${parent}`,
` Availability: ${availability}`,
` Best match: seq ${hit.bestMatch.seq} | ${hit.bestMatch.type} | ${hit.bestMatch.surface} | ${formatTime(hit.bestMatch.time)}`,
` Snippet: ${hit.bestMatch.snippet}`,
)
}
if (collected.capped) {
lines.push('', 'Result cap reached. Narrow the query or add filters to find additional matches.')
}
return lines.join('\n')
}
function formatEmptySessionSearch(): string {
return 'No prior session matches found.'
}
function formatEventSearch(
sessionId: SessionId,
title: TitleView,
collected: SearchCollection<SessionEventSearchHit>,
): string {
const lines = [`Session ${sessionId} — ${workspaceAccess.titleText(title)}`]
if (collected.items.length === 0) {
lines.push('', 'No prior event matches found.')
return lines.join('\n')
}
lines.push('', `Event search results (${collected.items.length}):`)
for (const [index, hit] of collected.items.entries()) {
lines.push(
`${index + 1}. seq ${hit.seq} | ${hit.type} | ${hit.surface} | ${formatTime(hit.time)}`,
` Snippet: ${hit.snippet}`,
)
}
if (collected.capped) {
lines.push('', 'Result cap reached. Narrow the query or add filters to find additional matches.')
}
return lines.join('\n')
}
function formatSessionTrace(
trace: SessionLineageTrace,
ancestors: readonly SessionRecord[],
ancestorBoundary: boolean,
descendants: AuthorizedDescendants,
titles: CompleteTitleMap,
): string {
const lines = [
`Session ${trace.target.header.id} — ${workspaceAccess.titleText(titles.get(trace.target.header.id))}`,
`Created: ${formatTime(trace.target.header.createdAt)}`,
`Availability: ${availabilityText(trace.target)}`,
'',
'Ancestors (nearest first):',
]
if (ancestors.length === 0 && !ancestorBoundary) lines.push('- none (target is a root session)')
for (const record of ancestors) {
lines.push(`- ${record.header.id} — ${workspaceAccess.titleText(titles.get(record.header.id))} | ${formatTime(record.header.createdAt)} | ${availabilityText(record)}`)
}
if (ancestorBoundary) lines.push('- [outside workspace boundary]')
lines.push('', 'Descendants:')
if (descendants.length === 0) lines.push('- none')
else renderDescendants(lines, descendants, titles)
return lines.join('\n')
}
function renderDescendants(
lines: string[],
nodes: AuthorizedDescendants,
titles: CompleteTitleMap,
): void {
for (const { node, depth } of workspaceAccess.visitDescendants(nodes)) {
const indent = ' '.repeat(depth)
if (node === null) {
lines.push(`${indent}- [outside workspace subtree]`)
continue
}
const id = node.record.header.id
lines.push(`${indent}- ${id} — ${workspaceAccess.titleText(titles.get(id))} | ${formatTime(node.record.header.createdAt)} | ${availabilityText(node.record)}`)
}
}
function formatEventTrace(
sessionId: SessionId,
title: TitleView,
trace: SessionEventTraceObservation,
): string {
return [
`Session ${sessionId} — ${workspaceAccess.titleText(title)}`,
`Target: seq ${trace.target.seq} | ${trace.target.type} | ${trace.target.surface} | ${formatTime(trace.target.time)}`,
`Replaced by: ${trace.replacedBy ?? 'none'}`,
`Replacement chain: ${seqList(trace.replacementChain)}`,
`Events replaced by target: ${seqList(trace.replacedEventSeqs)}`,
`Direct provenance sources: ${seqList(trace.sourceEventSeqs)}`,
`Direct derived events: ${seqList(trace.derivedEventSeqs)}`,
].join('\n')
}
function formatEventRead(
sessionId: SessionId,
title: TitleView,
window: SessionEventWindow,
): string {
const before = window.events.filter(event => event.seq < window.target.seq)
const after = window.events.filter(event => event.seq > window.target.seq)
const lines = [
`Session ${sessionId} — ${workspaceAccess.titleText(title)}`,
`Target event seq ${window.target.seq}:`,
'```json',
JSON.stringify(window.target, null, 2),
'```',
]
if (before.length > 0) {
lines.push('', 'Before:')
for (const event of before) lines.push(formatNeighbor(event))
}
if (after.length > 0) {
lines.push('', 'After:')
for (const event of after) lines.push(formatNeighbor(event))
}
return lines.join('\n')
}
function formatNeighbor(event: SessionEvent): string {
const text = extractSessionEventText(event)
return `- seq ${event.seq} | ${event.type} | ${formatTime(event.time)}`
+ (text.length === 0 ? ' | (no semantic text)' : `\n ${text.replaceAll('\n', '\n ')}`)
}
function availabilityText(record: SessionRecord): string {
return [
record.live ? 'live' : undefined,
record.persisted ? 'persisted' : undefined,
].filter((value): value is string => value !== undefined).join(', ') || 'unavailable'
}
function seqList(values: readonly number[]): string {
return values.length === 0 ? 'none' : values.join(', ')
}
function formatTime(value: number): string {
return new Date(value).toISOString()
}
function presentSessionSearchCall(args: SessionSearchCallArgs): GenericCallView {
return { card: 'generic', kind: 'search', title: 'Search prior sessions', rawInput: args.query }
}
function presentEventSearchCall(args: EventSearchCallArgs): GenericCallView {
return { card: 'generic', kind: 'search', title: 'Search session events', rawInput: args.query }
}
function presentSessionTraceCall(args: SessionTargetCallArgs): GenericCallView {
return {
card: 'generic',
kind: 'read',
title: args.session_id === undefined ? 'Trace current session' : `Trace session ${args.session_id}`,
...args.session_id === undefined ? {} : { rawInput: args.session_id },
}
}
function presentEventTargetCall(
action: string,
args: EventTargetCallArgs,
): GenericCallView {
return {
card: 'generic',
kind: 'read',
title: `${action} ${args.seq}`,
rawInput: {
...args.session_id === undefined ? {} : { session_id: args.session_id },
seq: args.seq,
},
}
}
/** Text output and call-card presentation for every session-query tool. */
export const presentation = {
formatSessionSearch,
formatEmptySessionSearch,
formatEventSearch,
formatSessionTrace,
formatEventTrace,
formatEventRead,
presentSessionSearchCall,
presentEventSearchCall,
presentSessionTraceCall,
presentEventTargetCall,
}

View File

@@ -0,0 +1,171 @@
/**
* Session-query service error containment and model-safe translation.
*
* @module @deepseek-ai/dsh-tool-session-query/service-boundary
*/
import type { Context } from 'cordis'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import {
SessionQueryError,
type SessionQueryErrorCode,
} from '@deepseek-ai/dsh-session-query'
interface ModelSafeServiceFailure {
readonly code: SessionQueryErrorCode | 'SESSION_QUERY_TOOL_FAILED'
readonly message: string
}
const UNPRINTABLE_SERVICE_ERROR = '[unprintable session query failure]'
const SAFE_SESSION_QUERY_FAILURES = {
SESSION_QUERY_ABORTED: {
code: 'SESSION_QUERY_ABORTED',
message: 'session query was cancelled',
},
SESSION_QUERY_EVENT_NOT_FOUND: {
code: 'SESSION_QUERY_EVENT_NOT_FOUND',
message: 'session event was not found',
},
SESSION_QUERY_INDEX_FAILED: {
code: 'SESSION_QUERY_INDEX_FAILED',
message: 'session search index is unavailable',
},
SESSION_QUERY_INVALID_CONFIG: {
code: 'SESSION_QUERY_TOOL_FAILED',
message: 'session query operation failed',
},
SESSION_QUERY_INVALID_CURSOR: {
code: 'SESSION_QUERY_INVALID_CURSOR',
message: 'session search continuation is invalid',
},
SESSION_QUERY_INVALID_FILTER: {
code: 'SESSION_QUERY_INVALID_FILTER',
message: 'session query filters were rejected',
},
SESSION_QUERY_INVALID_LIMIT: {
code: 'SESSION_QUERY_INVALID_LIMIT',
message: 'session query result limit was rejected',
},
SESSION_QUERY_INVALID_QUERY: {
code: 'SESSION_QUERY_INVALID_QUERY',
message: 'session query was rejected',
},
SESSION_QUERY_INVALID_LINEAGE: {
code: 'SESSION_QUERY_INVALID_LINEAGE',
message: 'session lineage is invalid',
},
SESSION_QUERY_INVALID_SURFACE: {
code: 'SESSION_QUERY_INVALID_SURFACE',
message: 'session event history is invalid',
},
SESSION_QUERY_INVALID_WINDOW: {
code: 'SESSION_QUERY_INVALID_WINDOW',
message: 'session event window is invalid',
},
SESSION_QUERY_PERSISTENCE_FAILED: {
code: 'SESSION_QUERY_PERSISTENCE_FAILED',
message: 'session history storage is unavailable',
},
SESSION_QUERY_SESSION_NOT_FOUND: {
code: 'SESSION_QUERY_SESSION_NOT_FOUND',
message: 'session was not found',
},
SESSION_QUERY_STALE_CURSOR: {
code: 'SESSION_QUERY_STALE_CURSOR',
message: 'session history changed while paging; retry the complete search call',
},
SESSION_QUERY_SOURCE_CONFLICT: {
code: 'SESSION_QUERY_TOOL_FAILED',
message: 'session query operation failed',
},
} satisfies Record<SessionQueryErrorCode, ModelSafeServiceFailure>
function unauthorizedTarget(): HarnessError {
return new HarnessError(
'session target is outside the caller workspace',
'SESSION_QUERY_TOOL_UNAUTHORIZED',
)
}
async function call<Value>(
ctx: Context,
signal: AbortSignal,
operation: string,
invoke: () => Promise<Value>,
): Promise<Value> {
signal.throwIfAborted()
try {
const value = await invoke()
signal.throwIfAborted()
return value
} catch (error: unknown) {
signal.throwIfAborted()
throw sanitizeError(ctx, operation, error)
}
}
function sanitizeError(
ctx: Context,
operation: string,
error: unknown,
): HarnessError {
const generic = genericFailure()
const diagnostic = fullError(error)
try {
ctx.logger.warn(`tool-session-query: ${operation} failed: ${diagnostic}`)
if (error instanceof SessionQueryError) {
const code: unknown = error.code
const failure = typeof code === 'string' && Object.hasOwn(SAFE_SESSION_QUERY_FAILURES, code)
? SAFE_SESSION_QUERY_FAILURES[code as SessionQueryErrorCode]
: undefined
if (failure !== undefined && failure.code !== 'SESSION_QUERY_TOOL_FAILED') {
return new SessionQueryError(failure.message, failure.code)
}
}
if (error instanceof HarnessError && error.code === 'SESSION_QUERY_TOOL_UNAUTHORIZED') {
return unauthorizedTarget()
}
} catch {
return generic
}
return generic
}
function genericFailure(): HarnessError {
return new HarnessError(
'session query operation failed',
'SESSION_QUERY_TOOL_FAILED',
)
}
function fullError(error: unknown): string {
try {
return renderFullError(error)
} catch {
return UNPRINTABLE_SERVICE_ERROR
}
}
function renderFullError(error: unknown): string {
if (!(error instanceof Error)) return String(error)
const diagnostics: string[] = []
const seen = new Set<Error>()
let current: unknown = error
while (current instanceof Error && !seen.has(current)) {
seen.add(current)
diagnostics.push(current.stack ?? String(current))
current = current.cause
}
/* v8 ignore next -- defensive containment for a cyclic Error.cause graph */
if (current instanceof Error) diagnostics.push('[circular error cause]')
else if (current !== undefined) diagnostics.push(renderFullError(current))
return diagnostics.join('\nCaused by: ')
}
/** Model-safe session-query invocation and error translation boundary. */
export const serviceBoundary = {
unauthorizedTarget,
call,
sanitizeError,
}

View File

@@ -0,0 +1,255 @@
/**
* Caller identity, workspace authorization, and visible lineage projection.
*
* @module @deepseek-ai/dsh-tool-session-query/workspace-access
*/
import type { Context } from 'cordis'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import {
SessionId,
type SessionEvent,
type SessionHeader,
type SessionId as SessionIdValue,
} from '@deepseek-ai/dsh-session'
import type {
SessionLineageNode,
SessionRecord,
} from '@deepseek-ai/dsh-session-query'
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
import { serviceBoundary } from './service-boundary.ts'
interface Caller {
readonly id: SessionIdValue
readonly header: SessionHeader
readonly events: readonly SessionEvent[]
}
interface TitleView {
readonly text: string
readonly unavailableCode?: string
}
interface CompleteTitleMap extends ReadonlyMap<SessionIdValue, TitleView> {
get(id: SessionIdValue): TitleView
}
interface AuthorizedDescendant {
readonly record: SessionRecord
readonly descendants: Array<AuthorizedDescendant | null>
}
interface DescendantProjectionFrame {
readonly node: SessionLineageNode
readonly target: Array<AuthorizedDescendant | null>
readonly next: DescendantProjectionFrame | undefined
}
interface DescendantVisit {
readonly node: AuthorizedDescendant | null
readonly depth: number
readonly next: DescendantVisit | undefined
}
function callerOf(exec: ToolRunContext): Caller {
const agent = exec.agent
if (agent === undefined) {
throw new HarnessError(
'session query tools require an agent-bound caller',
'SESSION_QUERY_TOOL_MISSING_AGENT',
)
}
return {
id: agent.session.id,
header: agent.session.header,
events: agent.session.events,
}
}
function targetId(args: { readonly session_id?: string }, caller: Caller): SessionIdValue {
return args.session_id === undefined ? caller.id : SessionId(args.session_id)
}
async function authorizeTarget(
ctx: Context,
caller: Caller,
target: SessionIdValue,
signal: AbortSignal,
): Promise<void> {
if (target === caller.id) return
const cwd = caller.header.cwd
if (cwd === undefined) throw serviceBoundary.unauthorizedTarget()
const records = await serviceBoundary.call(ctx, signal, 'target authorization', () =>
ctx.sessionQuery.filterSessions([
{ kind: 'id', values: [target] },
{ kind: 'cwd', values: [cwd] },
], signal))
if (records.length !== 1) throw serviceBoundary.unauthorizedTarget()
}
function recordAuthorized(record: SessionRecord, caller: Caller): boolean {
return headerAuthorized(record.header, caller)
}
function headerAuthorized(header: SessionHeader, caller: Caller): boolean {
if (header.id === caller.id) return header.cwd === caller.header.cwd
return caller.header.cwd !== undefined && header.cwd === caller.header.cwd
}
function assertObservedTargetAuthorized(
caller: Caller,
target: SessionIdValue,
observed: SessionHeader,
): void {
if (observed.id !== target || !headerAuthorized(observed, caller)) {
throw serviceBoundary.unauthorizedTarget()
}
}
async function authorizeSessionIds(
ctx: Context,
caller: Caller,
ids: readonly SessionIdValue[],
signal: AbortSignal,
): Promise<ReadonlySet<SessionIdValue>> {
const unique = [...new Set(ids)]
const authorized = new Set<SessionIdValue>()
if (unique.includes(caller.id)) authorized.add(caller.id)
const cwd = caller.header.cwd
const other = unique.filter(id => id !== caller.id)
if (cwd === undefined || other.length === 0) return authorized
const records = await serviceBoundary.call(ctx, signal, 'session-id authorization', () =>
ctx.sessionQuery.filterSessions([
{ kind: 'id', values: other },
{ kind: 'cwd', values: [cwd] },
], signal))
const requested = new Set(other)
for (const record of records) {
if (requested.has(record.header.id) && recordAuthorized(record, caller)) {
authorized.add(record.header.id)
}
}
return authorized
}
async function readTitles(
ctx: Context,
caller: Caller,
ids: readonly SessionIdValue[],
signal: AbortSignal,
): Promise<CompleteTitleMap> {
const result = new Map<SessionIdValue, TitleView>()
const observations = await serviceBoundary.call(ctx, signal, 'title observation', () =>
ctx.sessionQuery.readTitleSnapshots(ids, signal))
for (const observation of observations) {
if (observation.status === 'rejected') {
result.set(observation.sessionId, unavailableTitle(ctx, observation.reason))
continue
}
assertObservedTargetAuthorized(caller, observation.sessionId, observation.value.session)
result.set(observation.sessionId, { text: observation.value.title?.title ?? 'untitled' })
}
return result as CompleteTitleMap
}
async function readTitle(
ctx: Context,
caller: Caller,
id: SessionIdValue,
signal: AbortSignal,
): Promise<TitleView> {
return (await readTitles(ctx, caller, [id], signal)).get(id)
}
function unavailableTitle(
ctx: Context,
error: unknown,
): TitleView {
const sanitized = serviceBoundary.sanitizeError(ctx, 'title observation item', error)
if (sanitized.code === 'SESSION_QUERY_TOOL_UNAUTHORIZED') throw sanitized
return { text: 'untitled', unavailableCode: sanitized.code }
}
function authorizeDescendants(
nodes: readonly SessionLineageNode[],
caller: Caller,
): Array<AuthorizedDescendant | null> {
const result: Array<AuthorizedDescendant | null> = []
let pending: DescendantProjectionFrame | undefined
for (const node of [...nodes].reverse()) {
pending = { node, target: result, next: pending }
}
while (pending !== undefined) {
const current = pending
pending = current.next
if (!recordAuthorized(current.node.session, caller)) {
current.target.push(null)
continue
}
const projected: AuthorizedDescendant = {
record: current.node.session,
descendants: [],
}
current.target.push(projected)
for (const child of [...current.node.descendants].reverse()) {
pending = {
node: child,
target: projected.descendants,
next: pending,
}
}
}
return result
}
function * visitDescendants(
nodes: readonly (AuthorizedDescendant | null)[],
): Generator<DescendantVisit> {
let pending: DescendantVisit | undefined
for (const node of [...nodes].reverse()) {
pending = { node, depth: 0, next: pending }
}
while (pending !== undefined) {
const current = pending
pending = current.next
yield current
if (current.node === null) continue
for (const child of [...current.node.descendants].reverse()) {
pending = {
node: child,
depth: current.depth + 1,
next: pending,
}
}
}
}
function descendantIds(nodes: readonly (AuthorizedDescendant | null)[]): SessionIdValue[] {
const ids: SessionIdValue[] = []
for (const { node } of visitDescendants(nodes)) {
if (node !== null) ids.push(node.record.header.id)
}
return ids
}
function titleText(view: TitleView): string {
return view.unavailableCode === undefined
? view.text
: `${view.text} (title unavailable: ${view.unavailableCode})`
}
/** Workspace-scoped caller authorization, title access, and lineage projection. */
export const workspaceAccess = {
callerOf,
targetId,
authorizeTarget,
recordAuthorized,
assertObservedTargetAuthorized,
authorizeSessionIds,
readTitles,
readTitle,
authorizeDescendants,
visitDescendants,
descendantIds,
titleText,
}

View File

@@ -0,0 +1,228 @@
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, {
SESSION_FORMAT_VERSION,
SessionId,
type Session,
} from '@deepseek-ai/dsh-session'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
const temporaryDirectories: string[] = []
const contexts: Context[] = []
afterEach(async () => {
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
for (const directory of temporaryDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true })
}
})
function fakeAgent(session: Session): Agent {
return { id: session.id, session } as unknown as Agent
}
describe('tool-session-query with the real SQLite provider', () => {
it('searches live prior-step history and a persisted same-workspace log', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-tool-session-query-'))
temporaryDirectories.push(root)
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
await ctx.plugin(SessionQuerySqlite, { path: join(root, 'session-query.db') })
await ctx.plugin(ToolSessionQuery)
const persisted = SessionId('persisted')
await ctx.sessionPersistence.create({
version: SESSION_FORMAT_VERSION,
id: persisted,
createdAt: 1,
cwd: '/work',
})
await ctx.sessionPersistence.append(persisted, [{
type: 'user/message',
seq: 0,
time: 2,
data: {
content: [{ type: 'text', text: 'persisted integration needle' }],
source: { kind: 'user' },
},
surfaceOp: 'append',
}])
const caller = ctx.sessions.create(SessionId('caller'), {
meta: { createdAt: 10, cwd: '/work' },
})
caller.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
caller.append(
'user/message',
{ content: [{ type: 'text', text: 'live integration needle' }], source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
caller.append('step/start', { turn: 1, step: 1 })
let call = 0
const execute = (name: string, args: unknown) => ctx.tools.execute({
name,
arguments: args,
callId: CallId(`integration-${++call}`),
signal: new AbortController().signal,
agent: fakeAgent(caller),
})
const sessions = await execute('session_search', { query: 'persisted integration needle' })
expect(sessions.isError).toBe(false)
expect(sessions.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
.toContain('Session persisted')
const persistedEvents = await execute('session_event_search', {
session_id: persisted,
query: 'persisted integration needle',
})
expect(persistedEvents.isError).toBe(false)
expect(persistedEvents.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
.toContain('seq 0')
const liveEvents = await execute('session_event_search', { query: 'live integration needle' })
expect(liveEvents.isError).toBe(false)
expect(liveEvents.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
.toContain('seq 1')
})
it('passes finite fractional epoch-millisecond bounds through SQLite comparisons', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-tool-session-query-fractional-'))
temporaryDirectories.push(root)
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
await ctx.plugin(SessionQuerySqlite, { path: join(root, 'session-query.db') })
await ctx.plugin(ToolSessionQuery)
const base = Date.parse('2026-07-24T00:00:00.000Z')
const persisted = SessionId('fractional-persisted')
await ctx.sessionPersistence.create({
version: SESSION_FORMAT_VERSION,
id: persisted,
createdAt: base,
cwd: '/work',
})
await ctx.sessionPersistence.append(persisted, [
{
type: 'user/message',
seq: 0,
time: base + 123,
data: {
content: [{ type: 'text', text: 'fractional integration needle' }],
source: { kind: 'user' },
},
surfaceOp: 'append',
},
{
type: 'user/message',
seq: 1,
time: base + 124,
data: {
content: [{ type: 'text', text: 'fractional integration needle' }],
source: { kind: 'user' },
},
surfaceOp: 'append',
},
{
type: 'user/message',
seq: 2,
time: -124,
data: {
content: [{ type: 'text', text: 'pre-epoch fractional needle' }],
source: { kind: 'user' },
},
surfaceOp: 'append',
},
{
type: 'user/message',
seq: 3,
time: -123,
data: {
content: [{ type: 'text', text: 'pre-epoch fractional needle' }],
source: { kind: 'user' },
},
surfaceOp: 'append',
},
])
const caller = ctx.sessions.create(SessionId('fractional-caller'), {
meta: { createdAt: base + 1_000, cwd: '/work' },
})
let call = 0
const execute = (args: unknown) => ctx.tools.execute({
name: 'session_event_search',
arguments: args,
callId: CallId(`fractional-integration-${++call}`),
signal: new AbortController().signal,
agent: fakeAgent(caller),
})
const lowerBound = await execute({
session_id: persisted,
query: 'fractional integration needle',
time_from: '2026-07-24T00:00:00.12300001Z',
})
expect(lowerBound.isError).toBe(false)
const lowerText = lowerBound.content.map(block => block.type === 'text' ? block.text : '').join('\n')
expect(lowerText).toContain('seq 1')
expect(lowerText).not.toContain('seq 0')
const upperBound = await execute({
session_id: persisted,
query: 'fractional integration needle',
time_to: '2026-07-24T08:00:00.1239999+08:00',
})
expect(upperBound.isError).toBe(false)
const upperText = upperBound.content.map(block => block.type === 'text' ? block.text : '').join('\n')
expect(upperText).toContain('seq 0')
expect(upperText).not.toContain('seq 1')
const emptySameMillisecond = await execute({
session_id: persisted,
query: 'fractional integration needle',
time_from: '2026-07-24T00:00:00.12300001Z',
time_to: '2026-07-24T08:00:00.1239999+08:00',
})
expect(emptySameMillisecond.isError).toBe(false)
expect(emptySameMillisecond.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
.toContain('No prior event matches found.')
const preEpochLower = await execute({
session_id: persisted,
query: 'pre-epoch fractional needle',
time_from: '1969-12-31T23:59:59.87600001Z',
})
expect(preEpochLower.isError).toBe(false)
const preEpochLowerText = preEpochLower.content
.map(block => block.type === 'text' ? block.text : '').join('\n')
expect(preEpochLowerText).toContain('seq 3')
expect(preEpochLowerText).not.toContain('seq 2')
const preEpochUpper = await execute({
session_id: persisted,
query: 'pre-epoch fractional needle',
time_to: '1969-12-31T19:59:59.8769999-04:00',
})
expect(preEpochUpper.isError).toBe(false)
const preEpochUpperText = preEpochUpper.content
.map(block => block.type === 'text' ? block.text : '').join('\n')
expect(preEpochUpperText).toContain('seq 2')
expect(preEpochUpperText).not.toContain('seq 3')
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../llm/llm"
},
{
"path": "../../core/session"
},
{
"path": "../../core/tools"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../session-query"
},
{
"path": "../../support/invariants"
},
{
"path": "../../util/timeout"
}
]
}