Przeglądaj źródła

refactor(session-query): split model-facing tool modules

Hypatia May 2 miesięcy temu
rodzic
commit
3da324d1e2

+ 2 - 2
.agents/notes/implemented/feature/2026-07-24-model-facing-session-query-tools.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-2026-07-24-model-facing-session-query-tools.md: aea490f3569dd95bffb6ebbaae5a130e6440c281
-2026-07-24-model-facing-session-query-tools.zh.md: eae100375fe7abf91ba3e503808a6d98b540255e
+2026-07-24-model-facing-session-query-tools.md: bc9143150d1e17eda9eab7f4864ed3a2f4983157
+2026-07-24-model-facing-session-query-tools.zh.md: c8a0c70789f21e4bbca523b6acc81925fb17b604

+ 2 - 0
.agents/notes/implemented/feature/2026-07-24-model-facing-session-query-tools.md

@@ -12,6 +12,8 @@ The unified `ctx.sessionQuery` service exposes exact reads, filters, relationshi
 
 `@deepseek-ai/dsh-tool-session-query` is the model-facing consumer of `ctx.sessionQuery`. It registers five narrow read-only tools: `session_search`, `session_event_search`, `session_trace`, `session_event_trace`, and `session_event_read`. The package imports the interface rather than the SQLite implementation, owns model argument validation and readable text rendering, and contributes one concise prompt section that teaches the prior-history search and search-to-trace/read workflow.
 
+The package entrypoint is only the public composition root for configuration, prompt registration, and tool registration. Its internal modules follow the execution boundary: `input.ts` owns model schemas, normalization, and filter construction; `service-boundary.ts` contains provider calls and model-safe error translation; `workspace-access.ts` owns caller identity, workspace authorization, title access, and lineage projection; `operations.ts` orchestrates the five service workflows; and `presentation.ts` renders tool results and call cards. This keeps policy in its owning layer without changing the package contract.
+
 `session_search` groups full-text matches by session and exposes typed session and event metadata filters. `session_event_search` searches one session, defaulting to the caller's current session. `session_trace` returns the complete authorized ancestor chain and recursive descendant trees. `session_event_trace` returns every known positional replacement and direct provenance relationship for one event. `session_event_read` returns the exact target event as unabridged JSON and optionally summarizes a bounded raw-event window; omitted `before` and `after` values mean target-only.
 
 Model-facing filters use flat snake-case fields. Timestamps are timezone-qualified ISO 8601 strings at the tool boundary, convert to inclusive epoch-millisecond ranges for the service, and render as UTC ISO 8601. List values are ORed inside one filter while separate filters are ANDed. Requested parent ids are deduplicated and authority-filtered before FTS, so only parents in the caller workspace enter the provider clause; missing and cross-workspace guesses behave identically, while the root-session marker remains independently ORed into that clause. Event type strings remain open because `SessionEventMap` is merge-extensible; availability and event surface use closed values.

+ 2 - 0
.agents/notes/implemented/feature/2026-07-24-model-facing-session-query-tools.zh.md

@@ -12,6 +12,8 @@ Status: implemented
 
 `@deepseek-ai/dsh-tool-session-query` 是 `ctx.sessionQuery` 面向模型的消费者。它注册五个职责单一的只读工具:`session_search`、`session_event_search`、`session_trace`、`session_event_trace` 和 `session_event_read`。该包依赖接口而非 SQLite 实现,负责模型参数校验与易读文本渲染,并贡献一个精简的提示词段,说明历史搜索以及从搜索转向追踪/读取的工作流。
 
+该包入口仅作为配置、提示词注册与工具注册的公开组合根。内部模块沿执行边界划分:`input.ts` 负责模型 schema、规范化与过滤条件构造;`service-boundary.ts` 包含提供方调用与面向模型的安全错误转换;`workspace-access.ts` 负责调用者身份、工作区授权、标题访问与谱系投影;`operations.ts` 编排五个服务工作流;`presentation.ts` 渲染工具结果与调用卡片。这样可让策略留在其所属层,同时不改变包契约。
+
 `session_search` 按会话聚合全文匹配,并公开带类型的会话与事件元数据过滤条件。`session_event_search` 搜索一个会话,默认目标为调用者的当前会话。`session_trace` 返回完整的已授权祖先链与递归后代树。`session_event_trace` 返回一个事件所有已知的位置替换关系与直接来源关系。`session_event_read` 以未删节 JSON 返回准确的目标事件,并可选择汇总一个有界的原始事件窗口;省略 `before` 与 `after` 时只返回目标。
 
 面向模型的过滤条件使用扁平的 snake-case 字段。工具边界上的时间戳采用带时区的 ISO 8601 字符串,转换为服务使用的闭区间毫秒时间戳,并以 UTC ISO 8601 渲染。同一个过滤条件中的列表值按 OR 组合,不同过滤条件按 AND 组合。请求的父会话 id 会在 FTS 之前去重并按权限过滤,因此只有调用者工作区中的父会话会进入提供方条件;缺失与跨工作区的猜测具有相同行为,而根会话标记仍会独立按 OR 加入该条件。由于 `SessionEventMap` 可通过声明合并扩展,事件类型字符串保持开放;可用状态与事件表层使用封闭取值。

+ 1 - 1
docs/config-catalog.md

@@ -1560,7 +1560,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/session-query/tool-session-query/src/index.ts:52`](../packages/session-query/tool-session-query/src/index.ts)
+Source: [`packages/session-query/tool-session-query/src/index.ts:29`](../packages/session-query/tool-session-query/src/index.ts)
 
 ## `@deepseek-ai/dsh-tool-skill`
 

+ 19 - 1128
packages/session-query/tool-session-query/src/index.ts

@@ -6,35 +6,12 @@
 
 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 SessionEventSearchPage,
-  type SessionEventSearchHit,
-  type SessionEventSurface,
-  type SessionEventTraceObservation,
-  type SessionEventWindow,
-  type SessionLineageNode,
-  type SessionLineageTrace,
-  type SessionRecord,
-  type SessionResultFilter,
-  type SessionQueryErrorCode,
-  type SessionSearchCursor,
-  type SessionSearchHit,
-} from '@deepseek-ai/dsh-session-query'
-import { defineTool, type GenericCallView, type ToolRunContext } from '@deepseek-ai/dsh-tools'
+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'
@@ -67,126 +44,6 @@ interface ResolvedConfig {
   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>
-}
-
-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
-}
-
-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 }],
@@ -197,76 +54,6 @@ const PROMPT_TEXT =
   + '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.'
 
-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>
-
 /** Register all five tools and their shared model guidance. */
 export function apply(ctx: Context, config: Config): void {
   const resolved = resolveConfig(config)
@@ -279,59 +66,59 @@ export function apply(ctx: Context, config: Config): void {
   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,
+    parameters: toolInput.sessionSearchParameters,
     output: TEXT_OUTPUT,
     timeoutMs: resolved.searchTimeoutMs,
-    execute: (args, exec) => executeSessionSearch(ctx, args, exec, resolved.maxSearchResults),
-    presentCall: presentSessionSearchCall,
+    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: EVENT_SEARCH_PARAMETERS,
+    parameters: toolInput.eventSearchParameters,
     output: TEXT_OUTPUT,
     timeoutMs: resolved.searchTimeoutMs,
-    execute: (args, exec) => executeEventSearch(ctx, args, exec, resolved.maxSearchResults),
-    presentCall: presentEventSearchCall,
+    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: TARGET_SESSION_PARAMETER,
+    parameters: toolInput.targetSessionParameter,
     output: TEXT_OUTPUT,
     isConcurrencySafe: () => true,
-    execute: (args, exec) => executeSessionTrace(ctx, args, exec),
-    presentCall: presentSessionTraceCall,
+    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: {
-      ...TARGET_SESSION_PARAMETER,
+      ...toolInput.targetSessionParameter,
       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),
+    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: {
-      ...TARGET_SESSION_PARAMETER,
+      ...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) => executeEventRead(ctx, args, exec),
-    presentCall: args => presentEventTargetCall('Read event', args),
+    execute: (args, exec) => operations.executeEventRead(ctx, args, exec),
+    presentCall: args => presentation.presentEventTargetCall('Read event', args),
   }))
 }
 
@@ -348,899 +135,3 @@ function resolveConfig(config: Config): ResolvedConfig {
   }
   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()
-  const records = await sessionQueryCall(ctx, signal, 'target authorization', () =>
-    ctx.sessionQuery.filterSessions([
-      { kind: 'id', values: [target] },
-      { kind: 'cwd', values: [cwd] },
-    ], signal))
-  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 sessionQueryCall<Value>(
-  ctx: Context,
-  signal: AbortSignal,
-  operation: string,
-  call: () => Promise<Value>,
-): Promise<Value> {
-  signal.throwIfAborted()
-  try {
-    const value = await call()
-    signal.throwIfAborted()
-    return value
-  } catch (error: unknown) {
-    signal.throwIfAborted()
-    throw sanitizeSessionQueryError(ctx, operation, error)
-  }
-}
-
-function sanitizeSessionQueryError(
-  ctx: Context,
-  operation: string,
-  error: unknown,
-): HarnessError {
-  const generic = genericSessionQueryFailure()
-  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 genericSessionQueryFailure(): HarnessError {
-  return new HarnessError(
-    'session query operation failed',
-    'SESSION_QUERY_TOOL_FAILED',
-  )
-}
-
-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)
-  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 requestedParentIds = materializeParentSessionIds(args.parent_session_ids)
-  if (requestedParentIds !== undefined || args.include_root_sessions === true) {
-    const authorizedParentIds = requestedParentIds === undefined
-      ? new Set<SessionIdValue>()
-      : await authorizeSessionIds(ctx, caller, requestedParentIds, exec.signal)
-    const parentValues: Array<SessionIdValue | null> = requestedParentIds
-      ?.filter(id => authorizedParentIds.has(id)) ?? []
-    if (args.include_root_sessions === true) parentValues.push(null)
-    if (parentValues.length === 0) return formatEmptySessionSearch()
-    sessionFilters.push({ kind: 'parent', values: parentValues })
-  }
-  sessionFilters.push({ kind: 'cwd', values: [cwd] })
-  const collected = await collectPages(
-    maxResults,
-    exec.signal,
-    cursor => sessionQueryCall(ctx, exec.signal, 'session search', () =>
-      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, caller, 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, caller, 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,
-    async (cursor): Promise<SessionEventSearchPage> => {
-      const page = await sessionQueryCall(ctx, exec.signal, 'event search', () =>
-        ctx.sessionQuery.searchEvents({
-          sessionId,
-          query,
-          filters,
-          ...cursor === undefined ? {} : { cursor },
-        }, { signal: exec.signal }))
-      assertObservedTargetAuthorized(caller, sessionId, page.session)
-      return page
-    },
-    () => 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 sessionQueryCall(ctx, exec.signal, 'session lineage trace', () =>
-    ctx.sessionQuery.traceSession(sessionId, exec.signal))
-  assertObservedTargetAuthorized(caller, sessionId, trace.target.header)
-
-  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, caller, 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 sessionQueryCall(ctx, exec.signal, 'event trace', () =>
-    ctx.sessionQuery.traceEvent({ sessionId, seq: args.seq }, exec.signal))
-  assertObservedTargetAuthorized(caller, sessionId, trace.session)
-  const title = await readTitle(ctx, caller, 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 sessionQueryCall(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))
-  assertObservedTargetAuthorized(caller, sessionId, window.session)
-  const title = await readTitle(ctx, caller, 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.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))]
-}
-
-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 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)
-}
-
-/** Return the adjacent IEEE-754 value toward positive infinity for a finite input. */
-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)
-}
-
-/** Return the adjacent IEEE-754 value toward negative infinity for a finite input. */
-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',
-    )
-  }
-}
-
-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
-  }
-}
-
-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 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 sessionQueryCall(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 sessionQueryCall(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 = sanitizeSessionQueryError(ctx, 'title observation item', error)
-  if (sanitized.code === 'SESSION_QUERY_TOOL_UNAUTHORIZED') throw sanitized
-  return { text: 'untitled', unavailableCode: sanitized.code }
-}
-
-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: ')
-}
-
-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})`
-}
-
-function formatSessionSearch(
-  collected: SearchCollection<SessionSearchHit>,
-  titles: CompleteTitleMap,
-  authorizedParents: ReadonlySet<SessionIdValue>,
-): 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} — ${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: 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)
-  return lines.join('\n')
-}
-
-function renderDescendants(
-  lines: string[],
-  nodes: readonly (AuthorizedDescendant | null)[],
-  titles: CompleteTitleMap,
-): void {
-  for (const { node, depth } of 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} — ${titleText(titles.get(id))} | ${formatTime(node.record.header.createdAt)} | ${availabilityText(node.record)}`)
-  }
-}
-
-function formatEventTrace(
-  sessionId: SessionIdValue,
-  title: TitleView,
-  trace: SessionEventTraceObservation,
-): 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,
-    },
-  }
-}

+ 307 - 0
packages/session-query/tool-session-query/src/input.ts

@@ -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,
+}

+ 281 - 0
packages/session-query/tool-session-query/src/operations.ts

@@ -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,
+}

+ 255 - 0
packages/session-query/tool-session-query/src/presentation.ts

@@ -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,
+}

+ 171 - 0
packages/session-query/tool-session-query/src/service-boundary.ts

@@ -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,
+}

+ 255 - 0
packages/session-query/tool-session-query/src/workspace-access.ts

@@ -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,
+}