feat(session-query): add model-facing tools (round 1)

This commit is contained in:
Hypatia May
2026-07-24 15:09:55 +08:00
parent b92235c490
commit a350e95165
40 changed files with 2781 additions and 13 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

@@ -0,0 +1,72 @@
# @deepseek-ai/dsh-tool-session-query
Workspace-authorized model tools over `ctx.sessionQuery`. The package depends only on the unified interface and registers `session_search`, `session_event_search`, `session_trace`, `session_event_trace`, and `session_event_read`.
## 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. 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. 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.
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,57 @@
{
"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-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -0,0 +1,961 @@
/**
* 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 { HarnessError } from '@deepseek-ai/dsh-llm'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import {
SessionId,
type SessionEvent,
type SessionEventType,
type SessionHeader,
type SessionId as SessionIdValue,
} from '@deepseek-ai/dsh-session'
import {
SessionQueryError,
extractSessionEventText,
type SessionAvailability,
type SessionEventMetadataFilter,
type SessionEventSearchHit,
type SessionEventSurface,
type SessionEventTrace,
type SessionEventWindow,
type SessionLineageNode,
type SessionLineageTrace,
type SessionRecord,
type SessionResultFilter,
type SessionSearchCursor,
type SessionSearchHit,
} from '@deepseek-ai/dsh-session-query'
import { defineTool, type GenericCallView, type ToolRunContext } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-system-prompt'
/** 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
}
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 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 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 SearchCollection<T> {
readonly items: T[]
readonly capped: boolean
}
interface AuthorizedDescendant {
readonly record: SessionRecord
readonly descendants: Array<AuthorizedDescendant | null>
}
const SESSION_SEARCH_PARAMETERS = {
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 EVENT_SEARCH_PARAMETERS = {
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 TARGET_SESSION_PARAMETER = {
session_id: { type: 'string', description: 'Target session id. Omit for the current session.' },
} as const
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: SESSION_SEARCH_PARAMETERS,
output: TEXT_OUTPUT,
timeoutMs: resolved.searchTimeoutMs,
isConcurrencySafe: () => true,
execute: (args, exec) => executeSessionSearch(ctx, args, exec, resolved.maxSearchResults),
presentCall: 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: EVENT_SEARCH_PARAMETERS,
output: TEXT_OUTPUT,
timeoutMs: resolved.searchTimeoutMs,
isConcurrencySafe: () => true,
execute: (args, exec) => executeEventSearch(ctx, args, exec, resolved.maxSearchResults),
presentCall: 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: TARGET_SESSION_PARAMETER,
output: TEXT_OUTPUT,
isConcurrencySafe: () => true,
execute: (args, exec) => executeSessionTrace(ctx, args, exec),
presentCall: 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: {
...TARGET_SESSION_PARAMETER,
seq: { type: 'integer', required: true, description: 'Target event sequence number.' },
},
output: TEXT_OUTPUT,
isConcurrencySafe: () => true,
execute: (args, exec) => executeEventTrace(ctx, args, exec),
presentCall: args => 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: {
...TARGET_SESSION_PARAMETER,
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) => executeEventRead(ctx, args, exec),
presentCall: args => 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 }
}
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: SessionTargetArgs, 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 unauthorizedTarget()
signal.throwIfAborted()
const records = await ctx.sessionQuery.filterSessions([
{ kind: 'id', values: [target] },
{ kind: 'cwd', values: [cwd] },
])
signal.throwIfAborted()
if (records.length !== 1) throw unauthorizedTarget()
}
function unauthorizedTarget(): HarnessError {
return new HarnessError(
'session target is outside the caller workspace',
'SESSION_QUERY_TOOL_UNAUTHORIZED',
)
}
async function executeSessionSearch(
ctx: Context,
args: SessionSearchArgs,
exec: ToolRunContext,
maxResults: number,
): Promise<string> {
const caller = 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 = normalizeQuery(args.query)
const sessionFilters = buildSessionFilters(args)
sessionFilters.push({ kind: 'cwd', values: [cwd] })
const eventFilters = 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 collected = await collectPages(
maxResults,
exec.signal,
cursor => ctx.sessionQuery.searchSessions({
query,
sessionFilters,
eventFilters,
...cursor === undefined ? {} : { cursor },
}, { signal: exec.signal }),
hit => hit.header.id !== caller.id && recordAuthorized(hit, caller),
)
const parentIds = collected.items
.map(hit => hit.header.parentSession)
.filter((id): id is SessionIdValue => id !== undefined)
const authorizedParents = await authorizeSessionIds(ctx, caller, parentIds, exec.signal)
const titles = await readTitles(ctx, collected.items.map(hit => hit.header.id), exec.signal)
return formatSessionSearch(collected, titles, authorizedParents)
}
async function executeEventSearch(
ctx: Context,
args: EventSearchArgs,
exec: ToolRunContext,
maxResults: number,
): Promise<string> {
const caller = callerOf(exec)
const sessionId = targetId(args, caller)
await authorizeTarget(ctx, caller, sessionId, exec.signal)
const query = normalizeQuery(args.query)
const range = 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 readTitle(ctx, sessionId, exec.signal)
if (range.from !== undefined && range.to !== undefined && range.from > range.to) {
return formatEventSearch(sessionId, title, { items: [], capped: false })
}
const filters = 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,
cursor => ctx.sessionQuery.searchEvents({
sessionId,
query,
filters,
...cursor === undefined ? {} : { cursor },
}, { signal: exec.signal }),
() => true,
)
return formatEventSearch(sessionId, title, collected)
}
async function executeSessionTrace(
ctx: Context,
args: SessionTargetArgs,
exec: ToolRunContext,
): Promise<string> {
const caller = callerOf(exec)
const sessionId = targetId(args, caller)
await authorizeTarget(ctx, caller, sessionId, exec.signal)
const trace = await ctx.sessionQuery.traceSession(sessionId)
exec.signal.throwIfAborted()
const ancestors: SessionRecord[] = []
let ancestorBoundary = false
for (const ancestor of trace.ancestors) {
if (!recordAuthorized(ancestor, caller)) {
ancestorBoundary = true
break
}
ancestors.push(ancestor)
}
if (ancestors.length === trace.ancestors.length && !trace.complete) ancestorBoundary = true
const descendants = authorizeDescendants(trace.descendants, caller)
const visibleIds = [
trace.target.header.id,
...ancestors.map(record => record.header.id),
...descendantIds(descendants),
]
const titles = await readTitles(ctx, visibleIds, exec.signal)
return formatSessionTrace(trace, ancestors, ancestorBoundary, descendants, titles)
}
async function executeEventTrace(
ctx: Context,
args: EventTargetArgs,
exec: ToolRunContext,
): Promise<string> {
assertNonNegativeSafeInteger('seq', args.seq)
const caller = callerOf(exec)
const sessionId = targetId(args, caller)
await authorizeTarget(ctx, caller, sessionId, exec.signal)
const trace = await ctx.sessionQuery.traceEvent({ sessionId, seq: args.seq })
exec.signal.throwIfAborted()
const title = await readTitle(ctx, sessionId, exec.signal)
return formatEventTrace(sessionId, title, trace)
}
async function executeEventRead(
ctx: Context,
args: EventReadArgs,
exec: ToolRunContext,
): Promise<string> {
assertNonNegativeSafeInteger('seq', args.seq)
if (args.before !== undefined) assertNonNegativeSafeInteger('before', args.before)
if (args.after !== undefined) assertNonNegativeSafeInteger('after', args.after)
const caller = callerOf(exec)
const sessionId = targetId(args, caller)
await authorizeTarget(ctx, caller, sessionId, exec.signal)
const window = await ctx.sessionQuery.readEvent({
sessionId,
seq: args.seq,
...args.before === undefined ? {} : { before: args.before },
...args.after === undefined ? {} : { after: args.after },
})
exec.signal.throwIfAborted()
const title = await readTitle(ctx, sessionId, exec.signal)
return formatEventRead(sessionId, title, window)
}
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.parent_session_ids !== undefined || args.include_root_sessions === true) {
const values: Array<SessionIdValue | null> = []
if (args.parent_session_ids !== undefined) {
assertNonEmptyArray('parent_session_ids', args.parent_session_ids)
values.push(...args.parent_session_ids.map(SessionId))
}
if (args.include_root_sessions === true) values.push(null)
filters.push({ kind: 'parent', values })
}
if (args.availability !== undefined) {
assertNonEmptyArray('availability', args.availability)
filters.push({ kind: 'availability', values: args.availability })
}
return filters
}
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
}
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 fromMs = from === undefined ? undefined : parseIsoTimestamp(`${name}_from`, from)
const toMs = to === undefined ? undefined : parseIsoTimestamp(`${name}_to`, to)
if (fromMs !== undefined && toMs !== undefined && fromMs > toMs) {
throw invalidRange(name, 'from must be less than or equal to to')
}
return {
...fromMs === undefined ? {} : { from: fromMs },
...toMs === undefined ? {} : { to: toMs },
}
}
const ISO_TIMESTAMP =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|([+-])(\d{2}):(\d{2}))$/
function parseIsoTimestamp(name: string, value: string): number {
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 timestamp = Date.parse(value)
return timestamp
}
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',
)
}
}
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()
let page: Awaited<ReturnType<typeof request>>
try {
page = await request(cursor)
} catch (error: unknown) {
if (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_STALE_CURSOR') {
throw new SessionQueryError(
'session history changed while paging; retry the complete search call',
'SESSION_QUERY_STALE_CURSOR',
{ cause: error },
)
}
throw error
}
signal.throwIfAborted()
for (const item of page.items) {
if (!accept(item)) continue
items.push(item)
if (items.length === maxResults) {
return {
items,
capped: page.nextCursor !== undefined || item !== page.items.at(-1),
}
}
}
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
}
}
function recordAuthorized(record: SessionRecord, caller: Caller): boolean {
if (record.header.id === caller.id) return true
return caller.header.cwd !== undefined && record.header.cwd === caller.header.cwd
}
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
signal.throwIfAborted()
const records = await ctx.sessionQuery.filterSessions([
{ kind: 'id', values: other },
{ kind: 'cwd', values: [cwd] },
])
signal.throwIfAborted()
for (const record of records) authorized.add(record.header.id)
return authorized
}
async function readTitles(
ctx: Context,
ids: readonly SessionIdValue[],
signal: AbortSignal,
): Promise<CompleteTitleMap> {
const result = new Map<SessionIdValue, TitleView>()
for (const id of new Set(ids)) {
result.set(id, await readTitle(ctx, id, signal))
}
return result as CompleteTitleMap
}
async function readTitle(
ctx: Context,
id: SessionIdValue,
signal: AbortSignal,
): Promise<TitleView> {
signal.throwIfAborted()
try {
const title = await ctx.sessionQuery.readTitle(id)
signal.throwIfAborted()
return { text: title?.title ?? 'untitled' }
} catch (error: unknown) {
if (signal.aborted) signal.throwIfAborted()
const code = error instanceof HarnessError ? error.code : 'UNKNOWN'
ctx.logger.warn(`tool-session-query: title read failed for session "${id}": ${fullError(error)}`)
return { text: 'untitled', unavailableCode: code }
}
}
function fullError(error: unknown): string {
return error instanceof Error ? error.stack ?? String(error) : String(error)
}
function authorizeDescendants(
nodes: readonly SessionLineageNode[],
caller: Caller,
): Array<AuthorizedDescendant | null> {
return nodes.map((node) => {
if (!recordAuthorized(node.session, caller)) return null
return {
record: node.session,
descendants: authorizeDescendants(node.descendants, caller),
}
})
}
function descendantIds(nodes: readonly (AuthorizedDescendant | null)[]): SessionIdValue[] {
const ids: SessionIdValue[] = []
for (const node of nodes) {
if (node === null) continue
ids.push(node.record.header.id, ...descendantIds(node.descendants))
}
return ids
}
function titleText(view: TitleView): string {
return view.unavailableCode === undefined
? view.text
: `${view.text} (title unavailable: ${view.unavailableCode})`
}
function formatSessionSearch(
collected: SearchCollection<SessionSearchHit>,
titles: CompleteTitleMap,
authorizedParents: ReadonlySet<SessionIdValue>,
): string {
if (collected.items.length === 0) return 'No prior session matches found.'
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}${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 formatEventSearch(
sessionId: SessionIdValue,
title: TitleView,
collected: SearchCollection<SessionEventSearchHit>,
): string {
const lines = [`Session ${sessionId}${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: readonly (AuthorizedDescendant | null)[],
titles: CompleteTitleMap,
): string {
const lines = [
`Session ${trace.target.header.id}${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}${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, 0)
return lines.join('\n')
}
function renderDescendants(
lines: string[],
nodes: readonly (AuthorizedDescendant | null)[],
titles: CompleteTitleMap,
depth: number,
): void {
for (const node of 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}${titleText(titles.get(id))} | ${formatTime(node.record.header.createdAt)} | ${availabilityText(node.record)}`)
renderDescendants(lines, node.descendants, titles, depth + 1)
}
}
function formatEventTrace(
sessionId: SessionIdValue,
title: TitleView,
trace: SessionEventTrace,
): string {
return [
`Session ${sessionId}${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: SessionIdValue,
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}${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: SessionSearchArgs): GenericCallView {
return { card: 'generic', kind: 'search', title: 'Search prior sessions', rawInput: args.query }
}
function presentEventSearchCall(args: EventSearchArgs): GenericCallView {
return { card: 'generic', kind: 'search', title: 'Search session events', rawInput: args.query }
}
function presentSessionTraceCall(args: SessionTargetArgs): 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: EventTargetArgs,
): GenericCallView {
return {
card: 'generic',
kind: 'read',
title: `${action} ${args.seq}`,
rawInput: {
...args.session_id === undefined ? {} : { session_id: args.session_id },
seq: args.seq,
},
}
}

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,100 @@
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')
})
})

View File

@@ -0,0 +1,796 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context, type Fiber } from 'cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { CallId, HarnessError } from '@deepseek-ai/dsh-llm'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import SessionStore, {
SESSION_FORMAT_VERSION,
SessionId,
type Session,
type SessionHeader,
type SessionId as SessionIdValue,
} from '@deepseek-ai/dsh-session'
import SessionQueryService, {
SessionQueryError,
SessionSearchCursor,
type SessionEventSearchHit,
type SessionEventSearchRequest,
type SessionSearchExecContext,
type SessionSearchHit,
type SessionSearchPage,
type SessionSearchRequest,
} from '@deepseek-ai/dsh-session-query'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
const activeContexts: Context[] = []
afterEach(async () => {
vi.restoreAllMocks()
for (const ctx of activeContexts.splice(0)) await ctx.fiber.dispose()
FakeQuery.reset()
})
function header(id: string, cwd: string | undefined, createdAt = 1, parentSession?: SessionIdValue): SessionHeader {
return {
version: SESSION_FORMAT_VERSION,
id: SessionId(id),
createdAt,
...cwd === undefined ? {} : { cwd },
...parentSession === undefined ? {} : { parentSession },
}
}
function createSession(
ctx: Context,
id: string,
cwd: string | undefined,
createdAt = 1,
parentSession?: SessionIdValue,
): Session {
return ctx.sessions.create(SessionId(id), {
meta: {
createdAt,
...cwd === undefined ? {} : { cwd },
...parentSession === undefined ? {} : { parentSession },
},
})
}
function openStep(session: Session, text = 'prior needle'): void {
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append(
'user/message',
{ content: [{ type: 'text', text }], source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
session.append('step/start', { turn: 1, step: 1 })
}
function fakeAgent(session: Session): Agent {
return { id: session.id, session } as unknown as Agent
}
function sessionHit(
id: string,
cwd: string | undefined,
text = 'needle excerpt',
parentSession?: SessionIdValue,
): SessionSearchHit {
return {
header: header(id, cwd, 100, parentSession),
live: true,
persisted: false,
bestMatch: {
sessionId: SessionId(id),
seq: 4,
type: 'assistant/message',
time: 200,
surface: 'current',
snippet: text,
},
}
}
function eventHit(sessionId: SessionIdValue, seq: number, text = 'needle excerpt'): SessionEventSearchHit {
return {
sessionId,
seq,
type: 'user/message',
time: 200 + seq,
surface: 'current',
snippet: text,
}
}
class FakeQuery extends SessionQueryService {
static sessionSearch: (
request: SessionSearchRequest,
exec?: SessionSearchExecContext,
) => Promise<SessionSearchPage<SessionSearchHit>> = () => Promise.resolve({ items: [] })
static eventSearch: (
request: SessionEventSearchRequest,
exec?: SessionSearchExecContext,
) => Promise<SessionSearchPage<SessionEventSearchHit>> = () => Promise.resolve({ items: [] })
static sessionRequests: SessionSearchRequest[] = []
static eventRequests: SessionEventSearchRequest[] = []
static searchSignals: Array<AbortSignal | undefined> = []
static titles = new Map<SessionIdValue, string | Error>()
static reset(): void {
this.sessionSearch = () => Promise.resolve({ items: [] })
this.eventSearch = () => Promise.resolve({ items: [] })
this.sessionRequests = []
this.eventRequests = []
this.searchSignals = []
this.titles = new Map()
}
override searchSessions(
request: SessionSearchRequest,
exec?: SessionSearchExecContext,
): Promise<SessionSearchPage<SessionSearchHit>> {
FakeQuery.sessionRequests.push(request)
FakeQuery.searchSignals.push(exec?.signal)
return FakeQuery.sessionSearch(request, exec)
}
override searchEvents(
request: SessionEventSearchRequest,
exec?: SessionSearchExecContext,
): Promise<SessionSearchPage<SessionEventSearchHit>> {
FakeQuery.eventRequests.push(request)
FakeQuery.searchSignals.push(exec?.signal)
return FakeQuery.eventSearch(request, exec)
}
override async readTitle(sessionId: SessionIdValue) {
const value = FakeQuery.titles.get(sessionId)
if (value instanceof Error) throw value
if (value === undefined) return super.readTitle(sessionId)
return {
title: value,
messageSeqs: [],
source: { kind: 'fallback' as const },
eventSeq: 0,
updatedAt: 1,
}
}
}
interface Mounted {
readonly ctx: Context
readonly fiber: Fiber
readonly caller: Session
call(name: string, args: unknown, options?: { agent?: Agent; signal?: AbortSignal }): Promise<ToolExecutionResult>
}
async function mount(
config: ToolSessionQuery.Config = {},
callerCwd: string | null = '/work',
): Promise<Mounted> {
const ctx = new Context()
activeContexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(FakeQuery)
const fiber = await ctx.plugin(ToolSessionQuery, config)
const caller = createSession(ctx, 'caller', callerCwd ?? undefined, 10)
openStep(caller)
let calls = 0
return {
ctx,
fiber,
caller,
call: (toolName, args, options = {}) => ctx.tools.execute({
name: toolName,
arguments: args,
callId: CallId(`call-${++calls}`),
signal: options.signal ?? new AbortController().signal,
...options.agent === undefined ? { agent: fakeAgent(caller) } : { agent: options.agent },
}),
}
}
function text(result: ToolExecutionResult): string {
return result.content.map(block => block.type === 'text' ? block.text : '').join('\n')
}
function errorCode(result: ToolExecutionResult): string | undefined {
return result.isError ? result.error.info?.code : undefined
}
describe('registration and schemas', () => {
it('registers the five cursor-free tools, prompt, timeouts, and pure generic presenters, then disposes them', async () => {
const mounted = await mount({ maxSearchResults: 7, searchTimeoutMs: 1234 })
const names = mounted.ctx.tools.schemas().map(schema => schema.name)
expect(names).toEqual([
'session_search',
'session_event_search',
'session_trace',
'session_event_trace',
'session_event_read',
])
const sessionSchema = mounted.ctx.tools.schemas().find(schema => schema.name === 'session_search')
expect(sessionSchema?.parameters).not.toHaveProperty('properties.cursor')
expect(sessionSchema?.parameters).not.toHaveProperty('properties.limit')
expect(sessionSchema?.parameters).not.toHaveProperty('properties.cwd')
expect(mounted.ctx.tools.get('session_search')?.timeoutMs).toBe(1234)
expect(mounted.ctx.tools.get('session_trace')?.timeoutMs).toBeUndefined()
const safeArgs: Record<string, unknown> = {
session_search: { query: 'q' },
session_event_search: { query: 'q' },
session_trace: {},
session_event_trace: { seq: 0 },
session_event_read: { seq: 0 },
}
for (const name of names) {
expect(mounted.ctx.tools.get(name)?.isConcurrencySafe?.(safeArgs[name])).toBe(true)
}
expect(mounted.ctx.tools.get('session_search')?.output.render({}, 'rendered'))
.toEqual([{ type: 'text', text: 'rendered' }])
expect(mounted.ctx.tools.get('session_search')?.presentCall?.({ query: 'needle' }))
.toEqual({ card: 'generic', kind: 'search', title: 'Search prior sessions', rawInput: 'needle' })
expect(mounted.ctx.tools.get('session_event_search')?.presentCall?.({ query: 'needle' }))
.toEqual({ card: 'generic', kind: 'search', title: 'Search session events', rawInput: 'needle' })
expect(mounted.ctx.tools.get('session_trace')?.presentCall?.({}))
.toEqual({ card: 'generic', kind: 'read', title: 'Trace current session' })
expect(mounted.ctx.tools.get('session_trace')?.presentCall?.({ session_id: 'other' }))
.toEqual({ card: 'generic', kind: 'read', title: 'Trace session other', rawInput: 'other' })
expect(mounted.ctx.tools.get('session_event_trace')?.presentCall?.({ session_id: 'other', seq: 3 }))
.toEqual({
card: 'generic',
kind: 'read',
title: 'Trace event 3',
rawInput: { session_id: 'other', seq: 3 },
})
expect(mounted.ctx.tools.get('session_event_read')?.presentCall?.({ seq: 4 }))
.toEqual({ card: 'generic', kind: 'read', title: 'Read event 4', rawInput: { seq: 4 } })
const assembly = await mounted.ctx.systemPrompt.assemble()
expect(assembly.sections.find(section => section.name === 'tool:session-query')?.text)
.toContain('prior sessions')
await mounted.fiber.dispose()
expect(mounted.ctx.tools.schemas().map(schema => schema.name)).toEqual([])
expect((await mounted.ctx.systemPrompt.assemble()).sections.map(section => section.name))
.not.toContain('tool:session-query')
})
it('fails invalid direct config before registering anything', async () => {
const mounted = await mount()
for (const maxSearchResults of [0, 1.5, Number.NaN]) {
expect(() => { ToolSessionQuery.apply(mounted.ctx, { maxSearchResults }) })
.toThrow('maxSearchResults')
}
for (const searchTimeoutMs of [0, 1.5, Number.POSITIVE_INFINITY, MAX_TIMER_DELAY_MS + 1]) {
expect(() => { ToolSessionQuery.apply(mounted.ctx, { searchTimeoutMs }) })
.toThrow(`no greater than ${MAX_TIMER_DELAY_MS}`)
}
expect(() => { ToolSessionQuery.apply(new Context(), {}) }).toThrow()
})
it('expresses the complete Node timer range in the Loader config schema', () => {
expect(new ToolSessionQuery.Config({ searchTimeoutMs: MAX_TIMER_DELAY_MS }))
.toEqual({ maxSearchResults: 100, searchTimeoutMs: MAX_TIMER_DELAY_MS })
expect(() => new ToolSessionQuery.Config({ searchTimeoutMs: 1.5 })).toThrow()
expect(() => new ToolSessionQuery.Config({ searchTimeoutMs: MAX_TIMER_DELAY_MS + 1 })).toThrow()
})
})
describe('input validation and translation', () => {
it.each([
[{ query: ' ' }, 'SESSION_QUERY_INVALID_QUERY'],
[{ query: 'bad\0query' }, 'SESSION_QUERY_INVALID_QUERY'],
[{ query: 'q', session_ids: [] }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', parent_session_ids: [] }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', availability: [] }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', availability: ['archived'] }, 'INVALID_ARGS'],
[{ query: 'q', event_types: [] }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', event_surfaces: [] }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', event_surfaces: ['hidden'] }, 'INVALID_ARGS'],
[{ query: 'q', event_seq_from: -1 }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', event_seq_to: Number.MAX_SAFE_INTEGER + 1 }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', event_seq_from: 2, event_seq_to: 1 }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-07-24T10:00:00' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-02-30T10:00:00Z' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2100-02-29T10:00:00Z' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-04-31T10:00:00Z' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-01-01T24:00:00Z' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-01-01T00:60:00Z' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-01-01T00:00:60Z' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-01-01T00:00:00+24:00' }, 'SESSION_QUERY_INVALID_FILTER'],
[{ query: 'q', created_at_from: '2026-01-01T00:00:00+00:60' }, 'SESSION_QUERY_INVALID_FILTER'],
[{
query: 'q',
created_at_from: '2026-07-25T00:00:00Z',
created_at_to: '2026-07-24T00:00:00Z',
}, 'SESSION_QUERY_INVALID_FILTER'],
])('rejects invalid search arguments %#', async (args, code) => {
const mounted = await mount()
const result = await mounted.call('session_search', args)
expect(errorCode(result)).toBe(code)
})
it('normalizes the query and compiles inclusive session/event filters with one parent OR clause', async () => {
const mounted = await mount()
await mounted.call('session_search', {
query: ' alpha beta ',
session_ids: ['a', 'b'],
created_at_from: '2026-07-24T00:00:00+08:00',
created_at_to: '2026-07-24T01:00:00+08:00',
parent_session_ids: ['parent'],
include_root_sessions: true,
availability: ['live'],
event_seq_from: 2,
event_seq_to: 9,
event_time_from: '2026-07-24T00:00:00Z',
event_time_to: '2026-07-24T01:00:00Z',
event_types: ['plugin/open-event'],
event_surfaces: ['shadowed'],
})
expect(FakeQuery.sessionRequests).toHaveLength(1)
expect(FakeQuery.sessionRequests[0]).toEqual({
query: 'alpha beta',
sessionFilters: [
{ kind: 'id', values: ['a', 'b'] },
{
kind: 'created-at',
from: Date.parse('2026-07-24T00:00:00+08:00'),
to: Date.parse('2026-07-24T01:00:00+08:00'),
},
{ kind: 'parent', values: ['parent', null] },
{ kind: 'availability', values: ['live'] },
{ kind: 'cwd', values: ['/work'] },
],
eventFilters: [
{ kind: 'seq', from: 2, to: 9 },
{
kind: 'time',
from: Date.parse('2026-07-24T00:00:00Z'),
to: Date.parse('2026-07-24T01:00:00Z'),
},
{ kind: 'type', values: ['plugin/open-event'] },
{ kind: 'surface', values: ['shadowed'] },
],
})
})
it('compiles one-sided timestamps and independent root/parent clauses', async () => {
const mounted = await mount()
await mounted.call('session_search', {
query: 'q',
created_at_from: '2024-02-29T00:00Z',
include_root_sessions: true,
event_time_to: '2000-02-29T00:00Z',
})
expect(FakeQuery.sessionRequests[0]?.sessionFilters).toContainEqual({
kind: 'created-at',
from: Date.parse('2024-02-29T00:00Z'),
})
expect(FakeQuery.sessionRequests[0]?.sessionFilters).toContainEqual({
kind: 'parent',
values: [null],
})
expect(FakeQuery.sessionRequests[0]?.eventFilters).toContainEqual({
kind: 'time',
to: Date.parse('2000-02-29T00:00Z'),
})
await mounted.call('session_search', {
query: 'q',
parent_session_ids: ['parent'],
})
expect(FakeQuery.sessionRequests[1]?.sessionFilters).toContainEqual({
kind: 'parent',
values: ['parent'],
})
})
})
describe('workspace authority and lineage redaction', () => {
it('fails closed without an agent and for direct cross-workspace targets', async () => {
const mounted = await mount()
createSession(mounted.ctx, 'outside', '/outside')
const missing = await mounted.ctx.tools.execute({
name: 'session_trace',
arguments: {},
callId: CallId('missing-agent'),
signal: new AbortController().signal,
})
expect(errorCode(missing)).toBe('SESSION_QUERY_TOOL_MISSING_AGENT')
const denied = await mounted.call('session_event_read', { session_id: 'outside', seq: 0 })
expect(errorCode(denied)).toBe('SESSION_QUERY_TOOL_UNAUTHORIZED')
expect(text(denied)).not.toContain('session "outside"')
})
it('allows only self for a null-cwd caller and denies cross-session search', async () => {
const mounted = await mount({}, null)
const own = await mounted.call('session_trace', {})
expect(own.isError).toBe(false)
expect(text(own)).toContain('Session caller')
expect(errorCode(await mounted.call('session_search', { query: 'q' })))
.toBe('SESSION_QUERY_TOOL_UNAUTHORIZED')
createSession(mounted.ctx, 'other', undefined)
expect(errorCode(await mounted.call('session_trace', { session_id: 'other' })))
.toBe('SESSION_QUERY_TOOL_UNAUTHORIZED')
})
it('redacts an unauthorized ancestor and prunes unauthorized descendant subtrees without hidden ids', async () => {
const mounted = await mount()
const hiddenParent = createSession(mounted.ctx, 'hidden-parent-secret', '/outside')
const target = createSession(mounted.ctx, 'target', '/work', 20, hiddenParent.id)
const visible = createSession(mounted.ctx, 'visible-child', '/work', 30, target.id)
const hidden = createSession(mounted.ctx, 'hidden-child-secret', '/outside', 40, target.id)
createSession(mounted.ctx, 'hidden-grandchild-secret', '/work', 50, hidden.id)
FakeQuery.titles.set(target.id, 'Target title')
FakeQuery.titles.set(visible.id, 'Visible title')
const result = await mounted.call('session_trace', { session_id: target.id })
const output = text(result)
expect(output).toContain('Target title')
expect(output).toContain('visible-child')
expect(output).toContain('[outside workspace boundary]')
expect(output).toContain('[outside workspace subtree]')
expect(output).not.toContain('hidden-parent-secret')
expect(output).not.toContain('hidden-child-secret')
expect(output).not.toContain('hidden-grandchild-secret')
})
it('renders authorized ancestors and an unresolved lineage boundary without leaking it', async () => {
const mounted = await mount()
const root = createSession(mounted.ctx, 'visible-root', '/work', 5)
const target = createSession(mounted.ctx, 'visible-target', '/work', 6, root.id)
const complete = text(await mounted.call('session_trace', { session_id: target.id }))
expect(complete).toContain('visible-root')
const missingParent = SessionId('missing-parent-secret')
const incomplete = createSession(mounted.ctx, 'incomplete-target', '/work', 7, missingParent)
const redacted = text(await mounted.call('session_trace', { session_id: incomplete.id }))
expect(redacted).toContain('[outside workspace boundary]')
expect(redacted).not.toContain(missingParent)
})
it('renders unavailable trace records and keeps a self-id descendant authorized', async () => {
const mounted = await mount()
const target = createSession(mounted.ctx, 'trace-unavailable', '/work')
const [record] = await mounted.ctx.sessionQuery.filterSessions([{ kind: 'id', values: [target.id] }])
const [callerRecord] = await mounted.ctx.sessionQuery.filterSessions([{
kind: 'id',
values: [mounted.caller.id],
}])
if (record === undefined || callerRecord === undefined) throw new Error('expected live records')
const unavailable = { ...record, live: false, persisted: false }
const persisted = { ...callerRecord, live: false, persisted: true }
vi.spyOn(mounted.ctx.sessionQuery, 'traceSession').mockResolvedValue({
target: unavailable,
ancestors: [],
descendants: [{ session: persisted, descendants: [] }],
complete: true,
root: unavailable,
})
const output = text(await mounted.call('session_trace', { session_id: target.id }))
expect(output).toContain('Availability: unavailable')
expect(output).toContain(mounted.caller.id)
expect(output).toContain('persisted')
})
})
describe('search paging, prior-history bounds, titles, and cancellation', () => {
it('drains hidden internal pages to the authorized non-self cap and masks an unauthorized parent id', async () => {
const mounted = await mount({ maxSearchResults: 2 })
const outside = createSession(mounted.ctx, 'outside-parent-secret', '/outside')
const a = createSession(mounted.ctx, 'a', '/work')
const b = createSession(mounted.ctx, 'b', '/work')
FakeQuery.titles.set(a.id, 'Alpha')
FakeQuery.titles.set(b.id, 'Beta')
const c1 = SessionSearchCursor('c1')
const c2 = SessionSearchCursor('c2')
FakeQuery.sessionSearch = (request) => {
if (request.cursor === undefined) {
return Promise.resolve({
items: [
sessionHit('caller', '/work'),
sessionHit('unauthorized', '/outside'),
],
nextCursor: c1,
})
}
if (request.cursor === c1) {
return Promise.resolve({
items: [sessionHit('a', '/work', 'first', outside.id)],
nextCursor: c2,
})
}
return Promise.resolve({
items: [sessionHit('b', '/work', 'second')],
nextCursor: SessionSearchCursor('more'),
})
}
const result = await mounted.call('session_search', { query: 'needle' })
const output = text(result)
expect(FakeQuery.sessionRequests).toHaveLength(3)
expect(FakeQuery.sessionRequests.every(request => request.limit === undefined)).toBe(true)
expect(FakeQuery.sessionRequests.map(request => request.cursor)).toEqual([undefined, c1, c2])
expect(output).toContain('Session a — Alpha')
expect(output).toContain('Session b — Beta')
expect(output).toContain('Parent: [outside workspace]')
expect(output).not.toContain('outside-parent-secret')
expect(output).toContain('Result cap reached')
})
it('preserves stale-cursor diagnostics without transparently restarting', async () => {
const mounted = await mount({ maxSearchResults: 2 })
const cursor = SessionSearchCursor('stale-next')
FakeQuery.sessionSearch = request => request.cursor === undefined
? Promise.resolve({ items: [], nextCursor: cursor })
: Promise.reject(new SessionQueryError('stale provider generation', 'SESSION_QUERY_STALE_CURSOR'))
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_STALE_CURSOR')
expect(text(result)).toContain('retry the complete search call')
expect(FakeQuery.sessionRequests).toHaveLength(2)
})
it('rejects a repeated internal cursor instead of looping', async () => {
const mounted = await mount()
const cursor = SessionSearchCursor('repeat')
FakeQuery.sessionSearch = () => Promise.resolve({ items: [], nextCursor: cursor })
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_INVALID_CURSOR')
expect(FakeQuery.sessionRequests).toHaveLength(2)
})
it('renders authorized parent ids and all availability states', async () => {
const mounted = await mount({ maxSearchResults: 3 })
const parent = createSession(mounted.ctx, 'parent', '/work')
const child = createSession(mounted.ctx, 'child', '/work', 2, parent.id)
const callerChild = createSession(mounted.ctx, 'caller-child', '/work', 3, mounted.caller.id)
FakeQuery.sessionSearch = () => Promise.resolve({
items: [
{ ...sessionHit(child.id, '/work', 'both', parent.id), live: true, persisted: true },
{ ...sessionHit(callerChild.id, '/work', 'persisted', mounted.caller.id), live: false, persisted: true },
{ ...sessionHit('unavailable', '/work', 'neither'), live: false, persisted: false },
],
})
const output = text(await mounted.call('session_search', { query: 'needle' }))
expect(output).toContain('Parent: parent')
expect(output).toContain(`Parent: ${mounted.caller.id}`)
expect(output).toContain('Availability: live, persisted')
expect(output).toContain('Availability: persisted')
expect(output).toContain('Availability: unavailable')
})
it('intersects current-session search with the event before the latest step and leaves other targets unchanged', async () => {
const mounted = await mount()
FakeQuery.eventSearch = request => Promise.resolve({
items: [eventHit(request.sessionId, 1)],
})
await mounted.call('session_event_search', {
query: 'prior',
seq_from: 0,
seq_to: 99,
})
expect(FakeQuery.eventRequests[0]?.filters).toContainEqual({ kind: 'seq', from: 0, to: 1 })
const other = createSession(mounted.ctx, 'other', '/work')
await mounted.call('session_event_search', {
session_id: other.id,
query: 'prior',
seq_from: 0,
seq_to: 99,
})
expect(FakeQuery.eventRequests[1]?.filters).toContainEqual({ kind: 'seq', from: 0, to: 99 })
})
it('returns no current-session hits without calling FTS when the user range starts in the active step', async () => {
const mounted = await mount()
const result = await mounted.call('session_event_search', {
query: 'prior',
seq_from: 2,
})
expect(result.isError).toBe(false)
expect(text(result)).toContain('No prior event matches found.')
expect(FakeQuery.eventRequests).toEqual([])
})
it('requires a current step boundary and drains event pages to a capped result', async () => {
const mounted = await mount({ maxSearchResults: 2 })
const noStep = createSession(mounted.ctx, 'no-step', '/work')
const missing = await mounted.call(
'session_event_search',
{ query: 'q' },
{ agent: fakeAgent(noStep) },
)
expect(errorCode(missing)).toBe('SESSION_QUERY_TOOL_NO_CURRENT_STEP')
const other = createSession(mounted.ctx, 'paged-events', '/work')
const cursor = SessionSearchCursor('events-next')
FakeQuery.eventSearch = request => request.cursor === undefined
? Promise.resolve({ items: [eventHit(other.id, 1)], nextCursor: cursor })
: Promise.resolve({ items: [eventHit(other.id, 2), eventHit(other.id, 3)] })
const result = await mounted.call('session_event_search', {
session_id: other.id,
query: 'q',
})
expect(FakeQuery.eventRequests.map(request => request.cursor)).toEqual([undefined, cursor])
expect(text(result)).toContain('Result cap reached')
})
it('preserves base results when a title read fails, annotates the code, and logs the full error', async () => {
const mounted = await mount()
const hit = createSession(mounted.ctx, 'hit', '/work')
const failure = new HarnessError('title backend failed', 'TITLE_BACKEND')
FakeQuery.titles.set(hit.id, failure)
FakeQuery.sessionSearch = () => Promise.resolve({ items: [sessionHit(hit.id, '/work')] })
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(result.isError).toBe(false)
expect(text(result)).toContain('untitled (title unavailable: TITLE_BACKEND)')
expect(warn).toHaveBeenCalledWith(expect.stringContaining('title backend failed'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('HarnessError'))
})
it('reports unknown title failures and preserves an Error without a stack', async () => {
const mounted = await mount()
const first = createSession(mounted.ctx, 'unknown-title', '/work')
const second = createSession(mounted.ctx, 'stackless-title', '/work')
const stackless = new Error('stackless')
Object.defineProperty(stackless, 'stack', { value: undefined })
const readTitle = vi.spyOn(mounted.ctx.sessionQuery, 'readTitle')
.mockRejectedValueOnce('string failure')
.mockRejectedValueOnce(stackless)
FakeQuery.sessionSearch = () => Promise.resolve({
items: [
sessionHit(first.id, '/work'),
sessionHit(second.id, '/work'),
],
})
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(text(result)).toContain('title unavailable: UNKNOWN')
expect(readTitle).toHaveBeenCalledTimes(2)
expect(warn).toHaveBeenCalledWith(expect.stringContaining('string failure'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Error: stackless'))
})
it('does not downgrade cancellation during title enrichment', async () => {
const mounted = await mount()
const hit = createSession(mounted.ctx, 'abort-title', '/work')
const controller = new AbortController()
FakeQuery.sessionSearch = () => Promise.resolve({ items: [sessionHit(hit.id, '/work')] })
vi.spyOn(mounted.ctx.sessionQuery, 'readTitle').mockImplementation(() => {
controller.abort()
return Promise.reject(new Error('cancelled title'))
})
const result = await mounted.call('session_search', { query: 'needle' }, { signal: controller.signal })
expect(result.isError).toBe(true)
expect(text(result)).not.toContain('title unavailable')
})
it('passes the exact execution signal to every FTS page and stops on cancellation', async () => {
const mounted = await mount()
const controller = new AbortController()
let started!: () => void
const bodyStarted = new Promise<void>((resolve) => { started = resolve })
FakeQuery.sessionSearch = (_request, exec) => new Promise((_resolve, reject) => {
started()
exec?.signal?.addEventListener('abort', () => {
reject(new SessionQueryError('aborted', 'SESSION_QUERY_ABORTED'))
}, { once: true })
})
const pending = mounted.call('session_search', { query: 'needle' }, { signal: controller.signal })
await bodyStarted
controller.abort()
const result = await pending
expect(result.isError).toBe(true)
expect(errorCode(result)).toBe('SESSION_QUERY_ABORTED')
expect(FakeQuery.searchSignals).toEqual([controller.signal])
})
})
describe('trace and exact read rendering', () => {
it('renders every event relationship sequence and a UTC target timestamp', async () => {
const mounted = await mount()
const session = createSession(mounted.ctx, 'relationships', '/work')
session.append(
'user/message',
{ content: [{ type: 'text', text: 'source' }], source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
session.append(
'assistant/message',
{
turn: 1,
step: 1,
content: [{ type: 'text', text: 'replacement' }],
provenance: { provider: 'test', model: 'test' },
},
{ surfaceOp: { op: 'replace', start: 0, end: 0 }, sourceEventSeqs: [0] },
)
const result = await mounted.call('session_event_trace', { session_id: session.id, seq: 0 })
expect(text(result)).toContain('Replacement chain: 1')
expect(text(result)).toContain('Direct derived events: 1')
expect(text(result)).toContain(new Date(session.events[0]?.time ?? 0).toISOString())
})
it('renders unabridged fenced target JSON and readable semantic neighbor summaries', async () => {
const mounted = await mount()
const session = createSession(mounted.ctx, 'read', '/work')
session.append(
'user/message',
{ content: [{ type: 'text', text: 'before semantic text' }], source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
session.append(
'assistant/message',
{
turn: 1,
step: 1,
content: [{ type: 'text', text: 'target full text' }],
provenance: { provider: 'test', model: 'test' },
},
{ surfaceOp: 'append' },
)
session.append(
'context/message',
{ content: [{ type: 'text', text: 'after semantic text' }], source: { kind: 'plugin', plugin: 'test' } },
{ surfaceOp: 'append' },
)
const result = await mounted.call('session_event_read', {
session_id: session.id,
seq: 1,
before: 1,
after: 1,
})
const output = text(result)
expect(output).toContain('```json')
expect(output).toContain('"text": "target full text"')
expect(output).toContain('before semantic text')
expect(output).toContain('after semantic text')
expect(output).not.toContain('truncated')
})
it('renders empty event relationships and neighbors without semantic text', async () => {
const mounted = await mount()
const session = createSession(mounted.ctx, 'empty-relations', '/work')
session.append('step/start', { turn: 1, step: 1 })
session.append('step/end', { turn: 1, step: 1 })
const trace = text(await mounted.call('session_event_trace', {
session_id: session.id,
seq: 0,
}))
expect(trace).toContain('Replaced by: none')
expect(trace).toContain('Replacement chain: none')
const onlyAfter = text(await mounted.call('session_event_read', {
session_id: session.id,
seq: 0,
after: 1,
}))
expect(onlyAfter).not.toContain('Before:')
expect(onlyAfter).toContain('(no semantic text)')
const onlyBefore = text(await mounted.call('session_event_read', {
session_id: session.id,
seq: 1,
before: 1,
}))
expect(onlyBefore).toContain('Before:')
expect(onlyBefore).not.toContain('After:')
})
it.each([
['session_event_trace', { seq: -1 }],
['session_event_read', { seq: Number.MAX_SAFE_INTEGER + 1 }],
['session_event_read', { seq: 0, before: -1 }],
['session_event_read', { seq: 0, after: 1.5 }, 'INVALID_ARGS'],
])('rejects invalid exact-read integers for %s', async (name, args, expected = 'SESSION_QUERY_INVALID_FILTER') => {
const mounted = await mount()
expect(errorCode(await mounted.call(name, args))).toBe(expected)
})
})

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"
}
]
}