perf: reuse AI-vault path matchers and query and sort keys (#19469)

* perf: reuse AI-vault path matchers and query and sort keys

* test(ai-vault): add cross-platform path and sort-key differentials

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-09-08 19:43:36 -07:00
committed by GitHub
co-authored by m4air Neil
parent 0f4bca26d4
commit 063c1caa6f
4 changed files with 291 additions and 83 deletions
+42 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import type { AiVaultListResult, AiVaultSession } from './ai-vault-types'
import { isPathInsideOrEqual } from './cross-platform-path'
import {
aiVaultScanLimit,
aiVaultSessionDepthCovers,
@@ -83,3 +84,43 @@ describe('Agent Session History depth', () => {
expect(truncateAiVaultListResult(loaded, 'unlimited')).toBe(loaded)
})
})
describe('Agent Session History scope truncation', () => {
it('normalizes scope roots and candidate cwd once per truncation pass', () => {
const loaded = result(
Array.from({ length: 1000 }, (_, i) => session(`id-${i}`, `/other/${i}`, i))
)
const scopes = Array.from({ length: 100 }, (_, i) => `/repo/${i}`)
const normalize = vi.spyOn(String.prototype, 'normalize')
let selected: AiVaultListResult
try {
selected = truncateAiVaultListResult(loaded, 10, scopes)
expect(normalize.mock.calls.length).toBeLessThanOrEqual(1100)
} finally {
normalize.mockRestore()
}
expect(selected.sessions).toEqual(loaded.sessions.slice(0, 10))
})
it('selects scoped sessions identically to the pre-hoist path predicate', () => {
const cases = [
['C:\\Users\\Ada\\repo', 'c:/users/ada/repo/app'],
['C:/Users/Ada/repo/', 'C:\\Users\\Ada\\repo'],
['C:\\Users\\Ada\\repo', 'C:\\Users\\Ada\\repo-other'],
['//wsl.localhost/Ubuntu/home/Ada/Repo', '//wsl$/Ubuntu/home/Ada/Repo/App'],
['//wsl.localhost/Ubuntu/home/ada/repo', '//wsl.localhost/Debian/home/ada/repo'],
['/Users/ada/repo', '/Users/ada/repo//app/'],
['/Users/ada/repo', '/Users/ada/repository'],
['/', '/anywhere']
]
for (const [scope, cwd] of cases) {
const loaded = result([session('scoped', cwd!, 0)])
const selected = truncateAiVaultListResult(loaded, 0, [scope!])
expect({ scope, cwd, kept: selected.sessions.length === 1 }).toEqual({
scope,
cwd,
kept: isPathInsideOrEqual(scope!, cwd!)
})
}
})
})
+7 -2
View File
@@ -1,4 +1,7 @@
import { isPathInsideOrEqual } from './cross-platform-path'
import {
createNormalizedPathInsideOrEqualMatcher,
normalizeRuntimePathForComparison
} from './cross-platform-path'
import type { AiVaultListArgs, AiVaultListResult } from './ai-vault-types'
export const DEFAULT_AI_VAULT_SCAN_LIMIT = 1000
@@ -40,10 +43,12 @@ export function truncateAiVaultListResult(
}
const selectedIds = new Set(result.sessions.slice(0, depth).map((session) => session.id))
if (scopePaths.length > 0) {
const scopeMatchers = scopePaths.map(createNormalizedPathInsideOrEqualMatcher)
let scopedCount = 0
for (const session of result.sessions) {
const cwd = session.cwd
if (cwd && scopePaths.some((scopePath) => isPathInsideOrEqual(scopePath, cwd))) {
const normalizedCwd = cwd ? normalizeRuntimePathForComparison(cwd) : null
if (normalizedCwd !== null && scopeMatchers.some((matches) => matches(normalizedCwd))) {
selectedIds.add(session.id)
if (++scopedCount >= depth) {
break
+162 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import type { AiVaultSession } from './ai-vault-types'
import {
agentLabel,
@@ -9,6 +9,8 @@ import {
parseVaultQuery
} from './ai-vault-session-filters'
import { sessionPreviewSearchText } from './ai-vault-session-display'
import { isPathInsideOrEqual } from './cross-platform-path'
import { parseWslUncPath } from './wsl-paths'
const baseSession: AiVaultSession = {
id: 'claude:1',
@@ -198,3 +200,162 @@ describe('/shared ai-vault-session-filters (lifted core)', () => {
expect(sessionPreviewSearchText(baseSession)).toContain('scope tabs')
})
})
describe('/shared ai-vault-session-filters (hoisted matchers and sort keys)', () => {
it('prepares workspace path matching once for a large session filter pass', () => {
const sessions = Array.from({ length: 1000 }, (_, i) => ({
...baseSession,
id: String(i),
cwd: `/other/${i}`
}))
const activeWorktreePaths = Array.from({ length: 100 }, (_, i) => `/repo/${i}`)
const normalize = vi.spyOn(String.prototype, 'normalize')
try {
expect(
filterAiVaultSessions(sessions, {
query: '',
agents: ['claude'],
scope: 'workspace',
sort: 'updated',
activeWorktreePaths,
hideEmptySessions: false
})
).toEqual([])
expect(normalize.mock.calls.length).toBeLessThanOrEqual(1100)
} finally {
normalize.mockRestore()
}
})
it('does not read transcript previews for empty or field-only queries', () => {
let reads = 0
const sessions = Array.from({ length: 1000 }, (_, i) => ({
...baseSession,
id: String(i),
get previewMessages() {
reads++
return baseSession.previewMessages
}
}))
for (const query of ['', 'repo:repo', 'path:app']) {
expect(
filterAiVaultSessions(sessions, {
query,
agents: ['claude'],
scope: 'all',
sort: 'updated',
activeWorktreePaths: [],
hideEmptySessions: false
})
).toHaveLength(1000)
}
expect(reads).toBe(0)
expect(
filterAiVaultSessions(sessions, {
query: 'scope',
agents: ['claude'],
scope: 'all',
sort: 'updated',
activeWorktreePaths: [],
hideEmptySessions: false
})
).toHaveLength(1000)
expect(reads).toBeGreaterThan(0)
})
it('parses each sort timestamp once with original ordering, including invalid dates', () => {
const sessions = Array.from({ length: 2000 }, (_, i) => ({
...baseSession,
id: String(i),
updatedAt:
i % 131 === 0
? 'invalid'
: new Date(1700000000000 + ((i * 173) % 1999) * 1000).toISOString()
}))
const parse = vi.spyOn(Date, 'parse')
let actual: AiVaultSession[]
let expected: AiVaultSession[]
try {
expected = [...sessions].sort(
(a, b) => Date.parse(b.updatedAt ?? b.modifiedAt) - Date.parse(a.updatedAt ?? a.modifiedAt)
)
expect(parse.mock.calls.length).toBeGreaterThan(10_000)
parse.mockClear()
actual = filterAiVaultSessions(sessions, {
query: '',
agents: ['claude'],
scope: 'all',
sort: 'updated',
activeWorktreePaths: [],
hideEmptySessions: false
})
expect(parse).toHaveBeenCalledTimes(2000)
} finally {
parse.mockRestore()
}
actual.forEach((session, i) => expect(session).toBe(expected[i]))
})
it('matches workspace scope identically to the pre-hoist path predicate', () => {
// The hoisted matcher must agree with isPathInsideOrEqual on every spelling the
// supported hosts produce: Windows case/separator folding, trailing separators,
// both WSL UNC aliases, /mnt drive mounts and near-miss sibling directories.
const cases: [workspace: string, cwd: string][] = [
['C:\\Users\\Ada\\repo', 'c:/users/ada/repo/app'],
['C:/Users/Ada/repo/', 'C:\\Users\\Ada\\repo'],
['C:\\Users\\Ada\\repo', 'C:\\Users\\Ada\\repo-other'],
['C:\\', 'C:\\anything'],
['//wsl.localhost/Ubuntu/home/ada/repo', '//wsl$/Ubuntu/home/ada/repo/app'],
['//wsl.localhost/Ubuntu/home/Ada/Repo', '//wsl$/Ubuntu/home/Ada/Repo/App'],
['//wsl.localhost/Ubuntu/home/Ada/Repo', '//wsl$/Ubuntu/home/ada/repo/App'],
['//wsl$/Ubuntu/home/ada/repo', '//WSL.LOCALHOST/ubuntu/home/ada/repo'],
['//wsl.localhost/Ubuntu/home/ada/repo', '//wsl.localhost/Debian/home/ada/repo'],
['//wsl.localhost/Ubuntu/mnt/c/work', '/mnt/c/work/app'],
['/Users/ada/repo', '/Users/ada/repo//app/'],
['/Users/ada/repo', '/Users/Ada/repo/app'],
['/Users/ada/repo', '/Users/ada/repository'],
['/', '/anywhere']
]
for (const [workspace, cwd] of cases) {
const expected =
isPathInsideOrEqual(workspace, cwd) ||
(parseWslUncPath(workspace)
? isPathInsideOrEqual(parseWslUncPath(workspace)!.linuxPath, cwd)
: false)
const actual = filterAiVaultSessions([{ ...baseSession, cwd }], {
query: '',
agents: ['claude'],
scope: 'workspace',
sort: 'updated',
activeWorktreePaths: [workspace],
hideEmptySessions: false
})
expect({ workspace, cwd, matched: actual.length === 1 }).toEqual({
workspace,
cwd,
matched: expected
})
}
})
it('orders by creation time identically to the per-comparison parse', () => {
const sessions = Array.from({ length: 500 }, (_, i) => ({
...baseSession,
id: String(i),
createdAt:
i % 37 === 0 ? 'invalid' : new Date(1700000000000 + ((i * 91) % 499) * 1000).toISOString()
}))
const expected = [...sessions].sort(
(a, b) => Date.parse(b.createdAt ?? b.modifiedAt) - Date.parse(a.createdAt ?? a.modifiedAt)
)
const actual = filterAiVaultSessions(sessions, {
query: '',
agents: ['claude'],
scope: 'all',
sort: 'created',
activeWorktreePaths: [],
hideEmptySessions: false
})
actual.forEach((session, i) => expect(session).toBe(expected[i]))
})
})
+80 -79
View File
@@ -3,7 +3,7 @@
// Metro only watches mobile/ + repo-root src/shared, never src/renderer.
// INVARIANT: /shared is a leaf — this module must NOT import from src/renderer.
import {
isPathInsideOrEqual,
createNormalizedPathInsideOrEqualMatcher,
normalizeRuntimePathForComparison,
normalizeRuntimePathSeparators
} from './cross-platform-path'
@@ -79,45 +79,50 @@ export function filterAiVaultSessions(
const agentSet = new Set(filters.agents)
const parsedQuery = parseVaultQuery(filters.query)
const workspaceMatchers =
filters.scope === 'workspace'
? filters.activeWorktreePaths.map(createAiVaultWorkspaceMatcher)
: []
return sessions
.filter((session) => {
if (!agentSet.has(session.agent)) {
const filtered = sessions.filter((session) => {
if (!agentSet.has(session.agent)) {
return false
}
// Hide plain empty sessions, but keep sessions with resumable content
// (some parsers only learn turns from previews, e.g. Grok) and zero-turn
// sessions that still carry recoverable content (queued prompts /
// subagent transcripts) so a lost conversation is surfaced distinctly.
if (
filters.hideEmptySessions &&
!isAiVaultSessionResumableContent(session) &&
!isAiVaultSessionRecoverableEmpty(session)
) {
return false
}
if (filters.scope === 'workspace') {
const cwd = session.cwd
const normalizedCwd = cwd ? normalizeRuntimePathForComparison(cwd) : null
if (normalizedCwd === null || !workspaceMatchers.some((matches) => matches(normalizedCwd))) {
return false
}
// Hide plain empty sessions, but keep sessions with resumable content
// (some parsers only learn turns from previews, e.g. Grok) and zero-turn
// sessions that still carry recoverable content (queued prompts /
// subagent transcripts) so a lost conversation is surfaced distinctly.
if (
filters.hideEmptySessions &&
!isAiVaultSessionResumableContent(session) &&
!isAiVaultSessionRecoverableEmpty(session)
) {
}
if (filters.scope === 'project') {
if (!filters.activeProjectKey) {
return false
}
if (filters.scope === 'workspace') {
const cwd = session.cwd
if (
!cwd ||
!filters.activeWorktreePaths.some((pathValue) =>
isAiVaultSessionInWorkspacePath(pathValue, cwd)
)
) {
return false
}
if (filters.sessionProjectById?.get(session.id)?.key !== filters.activeProjectKey) {
return false
}
if (filters.scope === 'project') {
if (!filters.activeProjectKey) {
return false
}
if (filters.sessionProjectById?.get(session.id)?.key !== filters.activeProjectKey) {
return false
}
}
return matchesQuery(session, parsedQuery, filters)
})
.sort((left, right) => compareSessions(left, right, filters.sort))
}
return matchesQuery(session, parsedQuery, filters)
})
if (filtered.length < 2) {
return filtered
}
return filtered
.map((session) => ({ session, time: sessionSortTime(session, filters.sort) }))
.sort((left, right) => right.time - left.time)
.map(({ session }) => session)
}
export function groupAiVaultSessions(
@@ -206,48 +211,48 @@ function matchesQuery(
parsed: ParsedQuery,
filters: Pick<AiVaultSessionFilterState, 'sessionProjectById' | 'projectLabelByKey'>
): boolean {
const searchable = [
session.title,
session.sessionId,
session.agent,
session.branch,
session.model,
session.cwd,
session.filePath,
sessionPreviewSearchText(session)
]
.filter(Boolean)
.join(' ')
.toLowerCase()
if (parsed.terms.some((term) => !searchable.includes(term))) {
return false
if (parsed.terms.length > 0) {
const searchable = [
session.title,
session.sessionId,
session.agent,
session.branch,
session.model,
session.cwd,
session.filePath,
sessionPreviewSearchText(session)
]
.filter(Boolean)
.join(' ')
.toLowerCase()
if (parsed.terms.some((term) => !searchable.includes(term))) {
return false
}
}
const sessionProject = filters.sessionProjectById?.get(session.id)
const repoLabel = (
sessionProject?.kind === 'repo'
? (filters.projectLabelByKey?.get(sessionProject.key) ?? sessionProject.label)
: folderLabel(session.cwd)
).toLowerCase()
if (parsed.repoTerms.some((term) => !repoLabel.includes(term))) {
return false
if (parsed.repoTerms.length > 0) {
const sessionProject = filters.sessionProjectById?.get(session.id)
const repoLabel = (
sessionProject?.kind === 'repo'
? (filters.projectLabelByKey?.get(sessionProject.key) ?? sessionProject.label)
: folderLabel(session.cwd)
).toLowerCase()
if (parsed.repoTerms.some((term) => !repoLabel.includes(term))) {
return false
}
}
const pathSearch = `${session.cwd ?? ''} ${session.filePath}`.toLowerCase()
if (parsed.pathTerms.some((term) => !pathSearch.includes(term))) {
return false
if (parsed.pathTerms.length > 0) {
const pathSearch = `${session.cwd ?? ''} ${session.filePath}`.toLowerCase()
if (parsed.pathTerms.some((term) => !pathSearch.includes(term))) {
return false
}
}
return true
}
function compareSessions(left: AiVaultSession, right: AiVaultSession, sort: AiVaultSort): number {
const leftValue = sort === 'created' ? left.createdAt : left.updatedAt
const rightValue = sort === 'created' ? right.createdAt : right.updatedAt
const leftTime = Date.parse(leftValue ?? left.modifiedAt)
const rightTime = Date.parse(rightValue ?? right.modifiedAt)
return rightTime - leftTime
function sessionSortTime(session: AiVaultSession, sort: AiVaultSort): number {
const value = sort === 'created' ? session.createdAt : session.updatedAt
return Date.parse(value ?? session.modifiedAt)
}
function getGroupIdentity(
@@ -276,19 +281,15 @@ function getGroupIdentity(
return { key: folderGroupKey(session.cwd), label: folderLabel(session.cwd) }
}
function isAiVaultSessionInWorkspacePath(workspacePath: string, sessionCwd: string): boolean {
if (isPathInsideOrEqual(workspacePath, sessionCwd)) {
return true
}
function createAiVaultWorkspaceMatcher(workspacePath: string): (normalizedCwd: string) => boolean {
const matches = createNormalizedPathInsideOrEqualMatcher(workspacePath)
const workspaceWslPath = parseWslUncPath(workspacePath)
if (!workspaceWslPath) {
return false
return matches
}
// WSL agent transcripts record Linux cwd values even when Orca stores the
// active worktree as a Windows UNC path.
return isPathInsideOrEqual(workspaceWslPath.linuxPath, sessionCwd)
// WSL transcripts record Linux cwd even when the workspace uses a UNC path.
const matchesLinux = createNormalizedPathInsideOrEqualMatcher(workspaceWslPath.linuxPath)
return (cwd) => matches(cwd) || matchesLinux(cwd)
}
function tokenizeQuery(query: string): string[] {