fix(ai-vault): Session History Hand Off gives OpenCode sessions their full history (#41236)

OpenCode, OpenCode 2 and Z Code keep every conversation in one shared
database, so Session History could only hand off the row's preview
messages. The session's own host now copies that one conversation into
the per-chat hand-off snapshot the chat Hand Off already uses, and the
dialog points the new agent at it. Older hosts, SSH rows and failed
reads keep the preview.
This commit is contained in:
Kelvin Amoaba
2026-11-11 20:06:53 +00:00
committed by GitHub
parent 6dd068e672
commit 04537aabc6
15 changed files with 292 additions and 33 deletions
@@ -0,0 +1,73 @@
import {
buildOpenCodeSqliteCandidatePath,
splitOpenCodeSqliteCandidate
} from '../../shared/opencode-sqlite-session-path'
import {
AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS,
isAiVaultDatabaseAgent,
type AiVaultSessionHandoffArgs,
type AiVaultSessionHandoffResult
} from '../../shared/ai-vault/contracts/ai-vault-session-handoff'
import { formatWorkerTranscriptMessage } from '../../shared/worker-transcript-text'
import { isWslUncPath, toLinuxPath } from '../../shared/wsl-paths'
import { readOpenCodeNativeChatTranscriptFull } from '../native-chat/transcript-opencode'
import { writeAgentSessionContextSnapshot } from '../native-chat/agent-session-context-snapshots'
import { wakeNativeChatTranscriptDistro } from '../native-chat/native-chat-open-wake'
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
import { wslDistroForPath } from '../wsl/filesystem/wsl-unc-access-gate'
import { getAiVaultWslScanHomeDirs } from './ai-vault-scan-roots'
import { openCodeWslPath } from './scanners/opencode/reader-transport/session-scanner-opencode-wsl-client'
import { configureOpenCodeTranscriptWslReaders } from '../native-chat/transcript-opencode-database'
/** Copies one database-backed conversation into its own hand-off snapshot on this host,
* so the new agent never gets a path to the account-wide database. */
export async function writeAiVaultSessionHandoffSnapshot(
args: Pick<AiVaultSessionHandoffArgs, 'agent' | 'filePath' | 'sessionId'>
): Promise<AiVaultSessionHandoffResult> {
if (!isAiVaultDatabaseAgent(args.agent)) {
return {}
}
// Rows carry `<db>#<session id>` or the bare database plus sessionId; both must name a database file.
const candidate =
splitOpenCodeSqliteCandidate(args.filePath, args.agent) || !args.sessionId
? args.filePath
: buildOpenCodeSqliteCandidatePath(args.filePath, args.sessionId)
const row = splitOpenCodeSqliteCandidate(candidate, args.agent)
if (!row) {
return {}
}
try {
// Hand-off is a user act: start a stopped distro before the gated database read.
await wakeNativeChatTranscriptDistro(wslDistroForPath(row.dbPath))
// No one waits past the caller's budget, so stop reading then too.
const signal = AbortSignal.timeout(AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS)
if (openCodeWslPath(row.dbPath)) {
// This process reads WSL databases through per-distro readers; keep every known distro's.
const homes = (await getAiVaultWslScanHomeDirs()).slice(0, 32)
await configureOpenCodeTranscriptWslReaders([...homes, row.dbPath], signal)
}
const read = await readOpenCodeNativeChatTranscriptFull(
row.sessionId,
{ resolveDbPath: async () => row.dbPath },
signal,
args.agent === 'zcode' ? 'zcode' : undefined
)
if ('error' in read) {
return {}
}
const text = read.messages.map(formatWorkerTranscriptMessage).join('\n\n')
if (!text.trim()) {
return {}
}
const path = await writeAgentSessionContextSnapshot(
getProfileUserDataPath(),
`ai-vault:${candidate}`,
text
)
// A WSL row hands off to a WSL workspace, which reads the Windows temp file through /mnt.
return { transcriptPath: isWslUncPath(row.dbPath) ? toLinuxPath(path) : path }
} catch (error) {
console.warn('[ai-vault] could not save the session for hand-off', error)
return {}
}
}
+35
View File
@@ -0,0 +1,35 @@
import { isUnresolvedOwnerHostId, parseExecutionHostId } from '../../../shared/execution-host'
import {
AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS,
readAiVaultSessionHandoffResult,
type AiVaultSessionHandoffArgs,
type AiVaultSessionHandoffResult
} from '../../../shared/ai-vault/contracts/ai-vault-session-handoff'
import { writeAiVaultSessionHandoffSnapshot } from '../../ai-vault/session-handoff-snapshot'
import { callRuntimeEnvironment } from '../runtime-environments/runtime-environment-transport-routing'
import { getRuntimeEnvironmentRegistryRoot } from '../runtime-environments/runtime-environment-registry-root'
/** The snapshot is written on the session's own host, where the hand-off agent starts. */
export async function prepareAiVaultSessionHandoff(
args: AiVaultSessionHandoffArgs
): Promise<AiVaultSessionHandoffResult> {
const host = parseExecutionHostId(args.executionHostId)
if (host?.kind === 'ssh') {
return {}
}
if (host?.kind !== 'runtime') {
return writeAiVaultSessionHandoffSnapshot(args)
}
if (isUnresolvedOwnerHostId(host.id)) {
return {}
}
// An older host answers method_not_found; any failure leaves the preview hand-off.
const response = await callRuntimeEnvironment(
getRuntimeEnvironmentRegistryRoot(),
host.environmentId,
'aiVault.prepareSessionHandoff',
{ agent: args.agent, filePath: args.filePath, sessionId: args.sessionId },
AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS
).catch(() => null)
return readAiVaultSessionHandoffResult(response?.ok === true ? response.result : null)
}
+5
View File
@@ -13,6 +13,8 @@ import { tryEnsureStructuredAgentSessionHostForResume } from '../../runtime/agen
import { copyStructuredChatConversation } from '../../ai-vault/structured-chat-conversation-copy'
import { wakeNativeChatTranscriptDistro } from '../../native-chat/native-chat-open-wake'
import { wslDistroForPath } from '../../wsl/filesystem/wsl-unc-access-gate'
import { prepareAiVaultSessionHandoff } from './ai-vault-handoff'
import type { AiVaultSessionHandoffArgs } from '../../../shared/ai-vault/contracts/ai-vault-session-handoff'
export type AiVaultResumeHandlerOptions = {
ensureStructuredSessionOwnership?: () => Promise<void>
@@ -28,6 +30,9 @@ export function registerAiVaultResumeHandler(options: AiVaultResumeHandlerOption
'aiVault:prepareSessionResume',
(_event, args: AiVaultPrepareSessionResumeRequest) => prepareAiVaultSessionResume(args, options)
)
ipcMain.handle('aiVault:prepareSessionHandoff', (_event, args: AiVaultSessionHandoffArgs) =>
prepareAiVaultSessionHandoff(args)
)
}
export async function prepareAiVaultSessionResume(
@@ -20,6 +20,16 @@ function zcodeTranscriptDatabasePaths(wslHomeDirs: readonly string[]): string[]
return [homedir(), ...wslHomeDirs].map((home) => join(home, '.zcode', 'cli', 'db', 'db.sqlite'))
}
/** Readers for every listed WSL root; the registry drops any distro left out. */
export async function configureOpenCodeTranscriptWslReaders(
roots: readonly string[],
signal: AbortSignal
): Promise<void> {
const readers = await waitForPromiseWithSignal(prepareOpenCodeWslReaders(roots), signal)
signal.throwIfAborted()
configureOpenCodeWslReaders(readers)
}
export async function discoverOpenCodeTranscriptDatabase(
sessionId?: string,
signal?: AbortSignal,
@@ -98,12 +108,7 @@ export async function discoverOpenCodeTranscriptDatabase(
await waitForPromiseWithSignal(getAiVaultWslScanHomeDirs(), boundedSignal)
).slice(0, 32)
if (homes.length > 0) {
const readers = await waitForPromiseWithSignal(
prepareOpenCodeWslReaders(homes),
boundedSignal
)
boundedSignal.throwIfAborted()
configureOpenCodeWslReaders(readers)
await configureOpenCodeTranscriptWslReaders(homes, boundedSignal)
}
const wsl = await findSession(zcodeTranscriptDatabasePaths(homes))
if (wsl) {
@@ -141,12 +146,7 @@ export async function discoverOpenCodeTranscriptDatabase(
const primaryWsl = primary.filter((path) => openCodeWslPath(path))
const readerRoots = [...new Set([...homes, ...primaryWsl])]
if (readerRoots.length > 0) {
const readers = await waitForPromiseWithSignal(
prepareOpenCodeWslReaders(readerRoots),
boundedSignal
)
boundedSignal.throwIfAborted()
configureOpenCodeWslReaders(readers)
await configureOpenCodeTranscriptWslReaders(readerRoots, boundedSignal)
}
const wsl = await findSession([...primaryWsl, ...sources.flat()])
if (wsl) {
@@ -1,4 +1,5 @@
import { prepareStructuredChatCliFork } from '../../../../ai-vault/structured-chat-cli-fork'
import { writeAiVaultSessionHandoffSnapshot } from '../../../../ai-vault/session-handoff-snapshot'
import {
AiVaultSearchRequestSchema,
AiVaultSearchStatusRequestSchema,
@@ -33,6 +34,7 @@ import {
import {
AiVaultDeleteSessionParams,
AiVaultListSessionsParams,
AiVaultPrepareSessionHandoffParams,
AiVaultPrepareSessionResumeParams,
AiVaultSessionTitlesParams
} from '../../../../../shared/rpc-contract/ai-vault-params'
@@ -164,6 +166,12 @@ export const AI_VAULT_METHODS = [
)
}
}),
defineMethod({
name: 'aiVault.prepareSessionHandoff',
permission: 'workspace',
params: AiVaultPrepareSessionHandoffParams,
handler: (params) => writeAiVaultSessionHandoffSnapshot(params)
}),
defineMethod({
name: 'aiVault.deleteSession',
permission: 'workspace',
+6
View File
@@ -7,6 +7,10 @@ import type {
AiVaultDeleteSessionArgs,
AiVaultDeleteSessionResult
} from '../../shared/ai-vault/contracts/ai-vault-session-deletion'
import type {
AiVaultSessionHandoffArgs,
AiVaultSessionHandoffResult
} from '../../shared/ai-vault/contracts/ai-vault-session-handoff'
import type {
AiVaultFirstUserPromptArgs,
AiVaultFirstUserPromptResult,
@@ -50,6 +54,8 @@ export type AiVaultApi = {
prepareSessionResume: (
args: AiVaultPrepareSessionResumeRequest
) => Promise<AiVaultPrepareSessionResumeResult>
/** Saves a database-backed session's full conversation on its own host for a hand-off. */
prepareSessionHandoff: (args: AiVaultSessionHandoffArgs) => Promise<AiVaultSessionHandoffResult>
listSubagentSessions: (args: AiVaultSubagentListArgs) => Promise<AiVaultSubagentListResult>
getFirstUserPrompt: (args: AiVaultFirstUserPromptArgs) => Promise<AiVaultFirstUserPromptResult>
/** Moves a deletable session's transcript to the OS trash; local sessions only. */
+3
View File
@@ -16,6 +16,7 @@ import type {
} from '../../shared/ai-vault/contracts/ai-vault-types'
import type { AiVaultSessionTitlesArgs } from '../../shared/ai-vault/contracts/ai-vault-session-title'
import type { AiVaultPrepareSessionResumeRequest } from '../../shared/ai-vault/resume/ai-vault-resume-preparation'
import type { AiVaultSessionHandoffArgs } from '../../shared/ai-vault/contracts/ai-vault-session-handoff'
import type { PreloadApi } from '../api-types'
import {
AI_VAULT_SCAN_PROGRESS_CHANNEL,
@@ -54,6 +55,8 @@ export const aiVaultApi = {
ipcRenderer.invoke('aiVault:cancelListSessions', args),
prepareSessionResume: (args: AiVaultPrepareSessionResumeRequest) =>
ipcRenderer.invoke('aiVault:prepareSessionResume', args),
prepareSessionHandoff: (args: AiVaultSessionHandoffArgs) =>
ipcRenderer.invoke('aiVault:prepareSessionHandoff', args),
listSubagentSessions: (args: AiVaultSubagentListArgs) =>
ipcRenderer.invoke('aiVault:listSubagentSessions', args),
getFirstUserPrompt: (args: AiVaultFirstUserPromptArgs) =>
@@ -367,7 +367,7 @@ export default function AiVaultPanel({
/>
)}
</AiVaultPanelSearch>
{launchActions.continuationRequest && (
{launchActions.continuationOpen && (
<AgentSessionContinuationDialog
open
request={launchActions.continuationRequest}
@@ -1,5 +1,13 @@
import type { AgentSessionContinuationRequest } from '@/lib/agent-session/context/agent-session-continuation'
import {
buildAgentSessionContinuationPrompt,
type AgentSessionContinuationRequest
} from '@/lib/agent-session/context/agent-session-continuation'
import type { AiVaultSession } from '../../../../../../shared/ai-vault/contracts/ai-vault-types'
import {
AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS,
isAiVaultDatabaseAgent
} from '../../../../../../shared/ai-vault/contracts/ai-vault-session-handoff'
import { withTimeout } from '../../../../../../shared/promise-timeout-fallback'
export function canContinueAiVaultSessionInNewSession(
session: AiVaultSession,
@@ -7,15 +15,47 @@ export function canContinueAiVaultSessionInNewSession(
): boolean {
return Boolean(
targetWorktreeId &&
(vaultTranscriptPath(session) || session.previewMessages.some((message) => message.text.trim()))
// A database row's host saves its conversation when Hand Off opens, even with no preview text.
(isAiVaultDatabaseAgent(session.agent) ||
vaultTranscriptPath(session) ||
session.previewMessages.some((message) => message.text.trim()))
)
}
export function prepareAiVaultSessionContinuation(args: {
type AiVaultSessionContinuationTarget = {
session: AiVaultSession
targetWorktreeId: string
targetWorkspacePath: string
}): AgentSessionContinuationRequest {
}
/** A database row's host saves this one conversation first; null when nothing usable came back. */
export async function loadDatabaseAiVaultSessionContinuation(
target: AiVaultSessionContinuationTarget
): Promise<AgentSessionContinuationRequest | null> {
const { session } = target
const saved = await withTimeout(
window.api.aiVault.prepareSessionHandoff({
agent: session.agent,
filePath: session.filePath,
sessionId: session.sessionId,
executionHostId: session.executionHostId
}),
AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS,
{}
)
const request = prepareAiVaultSessionContinuation({
...target,
savedTranscriptPath: saved.transcriptPath
})
return buildAgentSessionContinuationPrompt(request.source, 'focused') ? request : null
}
export function prepareAiVaultSessionContinuation(
args: AiVaultSessionContinuationTarget & {
/** This conversation's own hand-off snapshot, for database-backed rows. */
savedTranscriptPath?: string
}
): AgentSessionContinuationRequest {
const { session, targetWorktreeId, targetWorkspacePath } = args
return {
source: {
@@ -23,7 +63,7 @@ export function prepareAiVaultSessionContinuation(args: {
sourceAgent: session.agent,
sourceTitle: session.title,
sourceWorkingDirectory: session.cwd,
transcriptPath: vaultTranscriptPath(session),
transcriptPath: args.savedTranscriptPath ?? vaultTranscriptPath(session),
// Use provider-authenticated prompts; preview user entries may contain tool results or injected skills.
lastPrompt: session.lastUserPrompt ?? null,
lastAssistantMessage: latestAssistantPreview(session)
@@ -48,8 +88,8 @@ function previewTranscript(session: AiVaultSession): string {
}
function vaultTranscriptPath(session: AiVaultSession): string | null {
// OpenCode vault paths identify its database or storage metadata, not a full transcript.
if (session.agent === 'opencode' || session.agent === 'opencode2') {
// These rows name an account-wide database; handing that path over would expose every conversation in it.
if (isAiVaultDatabaseAgent(session.agent)) {
return null
}
return session.filePath.trim() || null
@@ -1,8 +1,9 @@
import {
notifyAiVaultSessionNoContinuationContext,
notifyAiVaultSessionPreparationFailure,
resolveAiVaultSessionLaunchTargetOrNotify
} from './ai-vault-session-launch-feedback'
import { useCallback, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
buildAiVaultResumeCopyCommandForWorktree,
@@ -23,7 +24,11 @@ import {
} from '@/lib/agent-session/resume/history/ai-vault-session-resume-preparation'
import { translate } from '@/i18n/i18n'
import { describeAiVaultCliForkFailure } from './ai-vault-session-cli-fork'
import { prepareAiVaultSessionContinuation } from './ai-vault-session-continuation'
import {
loadDatabaseAiVaultSessionContinuation,
prepareAiVaultSessionContinuation
} from './ai-vault-session-continuation'
import { isAiVaultDatabaseAgent } from '../../../../../../shared/ai-vault/contracts/ai-vault-session-handoff'
import { activateAiVaultStructuredSession } from '@/lib/agent-session/open/activate-ai-vault-structured-session'
import { adoptsCliConversations } from '../../../../../../shared/structured-agent-cli-conversation-adoption'
@@ -54,8 +59,12 @@ export function useAiVaultSessionLaunchActions({
targetState: AiVaultSessionResumeTargetState
agentCmdOverrides?: Partial<Record<AiVaultAgent, string | null>>
}) {
const [continuationRequest, setContinuationRequest] =
useState<AgentSessionContinuationRequest | null>(null)
// An open dialog whose request is null is still saving the conversation on its host.
const [continuation, setContinuation] = useState<{
request: AgentSessionContinuationRequest | null
} | null>(null)
// Only the latest Hand Off may fill or close the dialog.
const continuationAttempt = useRef<object | null>(null)
const buildResumeCommand = useCallback(
(session: AiVaultSession, worktreeId?: string | null): string =>
@@ -245,20 +254,33 @@ export function useAiVaultSessionLaunchActions({
)
return
}
setContinuationRequest(
prepareAiVaultSessionContinuation({
session,
targetWorktreeId: targetId.worktreeId,
targetWorkspacePath
})
)
const target = { session, targetWorktreeId: targetId.worktreeId, targetWorkspacePath }
const attempt = {}
continuationAttempt.current = attempt
if (!isAiVaultDatabaseAgent(session.agent)) {
setContinuation({ request: prepareAiVaultSessionContinuation(target) })
return
}
setContinuation({ request: null })
void loadDatabaseAiVaultSessionContinuation(target).then((request) => {
if (continuationAttempt.current !== attempt) {
return
}
if (request) {
setContinuation({ request })
return
}
setContinuation(null)
notifyAiVaultSessionNoContinuationContext()
})
},
[activeWorktree?.id, activeWorktreeId, targetState]
)
const handleContinuationDialogOpenChange = useCallback((open: boolean): void => {
if (!open) {
setContinuationRequest(null)
continuationAttempt.current = null
setContinuation(null)
}
}, [])
@@ -269,7 +291,8 @@ export function useAiVaultSessionLaunchActions({
handleResumeInNewCli,
handleResumeInNewChat,
handleContinueInNewSession,
continuationRequest,
continuationOpen: continuation !== null,
continuationRequest: continuation?.request ?? null,
handleContinuationDialogOpenChange
}
}
@@ -19,6 +19,15 @@ export function notifyAiVaultSessionQueued(agent: AiVaultAgent): void {
)
}
export function notifyAiVaultSessionNoContinuationContext(): void {
toast.error(
translate(
'components.agentSessionContinuation.noContext',
'No session context is available to continue in a new session.'
)
)
}
export function notifyAiVaultSessionPreparationFailure(
error: unknown,
agent: AiVaultAgent,
@@ -8,6 +8,11 @@ import type {
AiVaultPrepareSessionResumeResult
} from '../../../../shared/ai-vault/resume/ai-vault-resume-preparation'
import type { AiVaultDeleteSessionArgs } from '../../../../shared/ai-vault/contracts/ai-vault-session-deletion'
import {
AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS,
readAiVaultSessionHandoffResult,
type AiVaultSessionHandoffArgs
} from '../../../../shared/ai-vault/contracts/ai-vault-session-handoff'
import type {
AiVaultSessionTitlesArgs,
AiVaultSessionTitlesResult
@@ -105,6 +110,18 @@ export function createWebAiVaultApi(): NonNullable<Partial<PreloadApi>['aiVault'
cancelListSessions: () => Promise.resolve(),
prepareSessionResume: (args: AiVaultPrepareSessionResumeRequest) =>
callRuntimeResult<AiVaultPrepareSessionResumeResult>('aiVault.prepareSessionResume', args),
// A browser reaches only its own paired runtime; an older one leaves the preview hand-off.
prepareSessionHandoff: async (args: AiVaultSessionHandoffArgs) => {
if (!addressesOwnRuntime(args.executionHostId)) {
return {}
}
const result = await callRuntimeResult<unknown>(
'aiVault.prepareSessionHandoff',
{ agent: args.agent, filePath: args.filePath, sessionId: args.sessionId },
AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS
).catch(() => null)
return readAiVaultSessionHandoffResult(result)
},
// Why: no server-side RPC for subagent transcript listing yet, so report an empty (not erroring) result.
listSubagentSessions: () => Promise.resolve({ sessions: [], issues: [] }),
// Why: full first-prompt re-parse is local-FS only; web/runtime falls back to preview text.
@@ -0,0 +1,31 @@
import type { ExecutionHostId } from '../../execution-host'
import { resolveNativeChatTranscriptAgent } from '../../native-chat/native-chat-agent-support'
import type { AiVaultAgent } from './ai-vault-types'
/** Agents whose rows name one conversation inside an account-wide database. */
export function isAiVaultDatabaseAgent(
agent: AiVaultAgent
): agent is Extract<AiVaultAgent, 'opencode' | 'opencode2' | 'zcode'> {
return resolveNativeChatTranscriptAgent(agent) === 'opencode'
}
/** How long a hand-off waits for the host's saved copy before using the preview. */
export const AI_VAULT_SESSION_HANDOFF_TIMEOUT_MS = 30_000
export type AiVaultSessionHandoffArgs = {
agent: AiVaultAgent
/** The database path, or `<db>#<session id>`. */
filePath: string
sessionId?: string
executionHostId?: ExecutionHostId
}
/** Absent `transcriptPath`: no saved copy (older host, SSH row, unreadable database); use the preview. */
export type AiVaultSessionHandoffResult = { transcriptPath?: string }
/** Reads a host's answer; anything but a non-empty path keeps the preview hand-off. */
export function readAiVaultSessionHandoffResult(value: unknown): AiVaultSessionHandoffResult {
const transcriptPath =
value && typeof value === 'object' && 'transcriptPath' in value ? value.transcriptPath : null
return typeof transcriptPath === 'string' && transcriptPath ? { transcriptPath } : {}
}
@@ -99,3 +99,10 @@ export const AiVaultDeleteSessionParams = z.object({
sessionId: z.string().min(1).max(512).optional(),
filePath: z.string().min(1).max(AI_VAULT_SCOPE_PATH_MAX_LENGTH)
})
// Only the row identity travels; the host reads its own database and writes the snapshot locally.
export const AiVaultPrepareSessionHandoffParams = z.object({
agent: z.enum(AI_VAULT_AGENTS),
sessionId: z.string().min(1).max(512).optional(),
filePath: z.string().min(1).max(AI_VAULT_SCOPE_PATH_MAX_LENGTH)
})
+2
View File
@@ -87,6 +87,7 @@ import { ReadVisualParams } from './agent-session-visual-params'
import {
AiVaultDeleteSessionParams,
AiVaultListSessionsParams,
AiVaultPrepareSessionHandoffParams,
AiVaultPrepareSessionResumeParams,
AiVaultSessionTitlesParams
} from './ai-vault-params'
@@ -887,6 +888,7 @@ export const RPC_PARAMS_BY_METHOD = {
'agentTeams.tmuxCompat': AgentTeamsTmuxCompat,
'aiVault.deleteSession': AiVaultDeleteSessionParams,
'aiVault.listSessions': AiVaultListSessionsParams,
'aiVault.prepareSessionHandoff': AiVaultPrepareSessionHandoffParams,
'aiVault.prepareSessionResume': AiVaultPrepareSessionResumeParams,
'aiVault.resolveSessionTitles': AiVaultSessionTitlesParams,
'aiVault.searchSessions': AiVaultSearchRequestSchema,