perf(ai-vault): count recent sessions for scan cutoffs

This commit is contained in:
m4air
2026-09-12 18:23:29 -07:00
committed by Neil
parent fbab61ec09
commit 674376b332
6 changed files with 439 additions and 35 deletions
@@ -0,0 +1,197 @@
#!/usr/bin/env node
// git show <base>:src/main/ai-vault/session-scanner.ts | node config/scripts/session-scan-cutoff-benchmark.mjs
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { performance } from 'node:perf_hooks'
import ts from 'typescript-api'
import { build } from 'esbuild'
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
const baselineSource = ts.createSourceFile(
'session-scanner.ts',
readFileSync(0, 'utf8'),
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS
)
const baselineFunction = baselineSource.statements.find(
(node) => ts.isFunctionDeclaration(node) && node.name?.text === 'canStopParsingSessions'
)
assert(baselineFunction, 'Pipe the baseline session-scanner.ts on stdin')
async function load(contents) {
const result = await build({
stdin: { contents, resolveDir: path.resolve('src/main/ai-vault'), loader: 'ts' },
platform: 'node',
format: 'esm',
bundle: true,
write: false
})
const encoded = Buffer.from(result.outputFiles[0].text).toString('base64')
return import(`data:text/javascript;base64,${encoded}`)
}
const [baselineModule, currentModule] = await Promise.all([
load(`import { sessionSortTime } from './session-scanner-accumulator';
export ${baselineFunction.getText(baselineSource)}`),
load(`export { canStopParsingSessions } from './session-scan-cutoff';
export { CodexSessionCollection } from './codex-session-root-dedup';`)
])
const baseline = baselineModule.canStopParsingSessions
const current = currentModule.canStopParsingSessions
const { CodexSessionCollection } = currentModule
let randomState = 91114
function random(bound) {
randomState = (Math.imul(randomState, 1664525) + 1013904223) >>> 0
return Math.floor((randomState / 2 ** 32) * bound)
}
function session(index, overrides = {}) {
return Object.freeze({
agent: 'claude',
executionHostId: 'local',
sessionId: `session-${index}`,
filePath: `/home/ada/.codex/sessions/rollout-${index}.jsonl`,
codexHome: null,
updatedAt: new Date(index).toISOString(),
modifiedAt: new Date(0).toISOString(),
...overrides
})
}
function collection(rows) {
const result = new CodexSessionCollection()
for (const row of rows) {
result.add(row)
}
return result
}
function check(sessions, limit, next) {
const rows = [...sessions.values()]
assert.equal(current(sessions, limit, next), baseline(sessions, limit, next))
assert.deepEqual([...sessions.values()], rows)
}
const dates = [
null,
'',
'invalid',
'1970-01-01T00:00:00Z',
'1970-01-01T00:00:02+00:00',
'-000001-01-01T00:00:00Z',
'+010000-01-01T00:00:00Z',
'-271821-04-20T00:00:00.000Z'
]
const limits = [0, -1, -3, 0.5, 1.5, Number.NaN, Infinity, -Infinity]
const nextTimes = [undefined, Number.NaN, Infinity, -Infinity, 0, 1, 2, 2000]
let comparisons = 0
for (let trial = 0; trial < 4_000; trial += 1) {
const sessions = new CodexSessionCollection()
const admitted = []
for (let batch = 0; batch < 10; batch += 1) {
const count = random(8)
for (let index = 0; index < count; index += 1) {
const id = random(12)
const row =
admitted.length && random(5) === 0
? admitted[random(admitted.length)]
: session(id, {
agent: random(3) ? 'codex' : 'claude',
executionHostId: random(4) ? 'local' : 'ssh:dev',
codexHome: random(2) ? null : '/custom',
updatedAt: random(3)
? new Date(random(5000) - 2500).toISOString()
: dates[random(dates.length)],
modifiedAt: random(4) ? new Date(random(5000)).toISOString() : 'invalid'
})
admitted.push(row)
sessions.add(row)
}
const limit = random(3) ? 1 + random(40) : limits[random(limits.length)]
const next = random(2) ? random(5000) - 2500 : nextTimes[random(nextTimes.length)]
check(sessions, limit, next)
comparisons += 1
}
}
console.log(`${comparisons} differential batch cutoffs passed.`)
function median(values) {
const sorted = values.toSorted((left, right) => left - right)
return (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
}
function measure(name, run, repeats) {
const expected = run(baseline)
assert.deepEqual(run(current), expected)
const sample = (cutoff) => {
let result
const start = performance.now()
for (let index = 0; index < repeats; index += 1) {
result = run(cutoff)
}
const elapsed = (performance.now() - start) / repeats
assert.deepEqual(result, expected)
return elapsed
}
sample(baseline)
sample(current)
const samples = { baseline: [], current: [] }
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'current')) {
for (const arm of pair) {
samples[arm].push(sample(arm === 'baseline' ? baseline : current))
}
}
return { name, beforeMs: median(samples.baseline), afterMs: median(samples.current) }
}
console.log(
JSON.stringify({ node: process.version, platform: process.platform, arch: process.arch })
)
const results = []
for (const count of [8, 100, 1_000, 2_000, 10_000]) {
for (const order of ['ordered', 'shuffled']) {
const rows = Array.from({ length: count }, (_, index) => session(count - index))
if (order === 'shuffled') {
for (let index = count - 1; index > 0; index -= 1) {
const other = random(index + 1)
;[rows[index], rows[other]] = [rows[other], rows[index]]
}
}
const sessions = collection(rows)
for (const next of [0, count]) {
results.push(
measure(
`${count} ${order} / ${next === 0 ? 'stop' : 'continue'}`,
(cutoff) => cutoff(sessions, Math.ceil(count / 2), next),
Math.max(20, Math.floor(30_000 / count))
)
)
}
}
}
for (const invalidIndex of [0, 999]) {
const sessions = collection(
Array.from({ length: 1_000 }, (_, index) =>
session(index, invalidIndex === index ? { updatedAt: 'invalid' } : {})
)
)
results.push(measure(`1000 invalid at ${invalidIndex}`, (cutoff) => cutoff(sessions, 500, 0), 50))
}
const rows = Array.from({ length: 2_000 }, () => session(random(2_000)))
results.push(
measure(
'2000-candidate scan cutoff + admission / limit1000',
(cutoff) => {
const sessions = new CodexSessionCollection()
let index = 0
while (index < rows.length && !cutoff(sessions, 1_000, 10_000)) {
const end = Math.min(rows.length, index + Math.min(8, Math.max(1, 1_000 - sessions.size)))
while (index < end) {
sessions.add(rows[index++])
}
}
return { parsed: index, sessions: sessions.size }
},
2
)
)
console.table(results)
console.log('Synthetic cutoff/admission CPU; excludes discovery, parsing, I/O and final sorting.')
+2 -18
View File
@@ -31,6 +31,7 @@ import { errorMessage } from './session-scanner-values'
import { mapRemoteScanBatches } from './remote-session-scan-batching'
import { throwIfAiVaultScanCancelled } from './ai-vault-scan-cancellation'
import { recordSessionScanIssue } from './session-scan-issues'
import { canStopParsingSessions } from './session-scan-cutoff'
import { refreshCodexTitleFromIndex } from './session-scanner-codex-cached-title'
import { limitRemoteScanFilesystemConcurrency } from './remote-session-scan-concurrency'
import { aiVaultScanLimit } from '../../shared/ai-vault-session-depth'
@@ -145,7 +146,7 @@ async function parseRemoteSessionCandidates(args: {
let index = 0
while (index < args.candidates.length) {
if (canStopParsingRemoteSessions(sessions, args.limit, args.candidates[index]?.file.mtimeMs)) {
if (canStopParsingSessions(sessions, args.limit, args.candidates[index]?.file.mtimeMs)) {
break
}
@@ -311,23 +312,6 @@ function normalizeRemoteScopePaths(scopePaths: readonly string[]): string[] {
return scopePaths.map((scopePath) => scopePath.trim()).filter(Boolean)
}
function canStopParsingRemoteSessions(
sessions: CodexSessionCollection,
limit: number,
nextCandidateMtimeMs: number | undefined
): boolean {
if (sessions.size < limit || typeof nextCandidateMtimeMs !== 'number') {
return false
}
const visibleCutoff = Array.from(sessions.values(), sessionSortTime)
.sort((left, right) => right - left)
.at(limit - 1)
// Transcript mtimes bound the remaining candidate order; once the visible
// cutoff is newer, older files cannot enter the unscoped top-N result.
return typeof visibleCutoff === 'number' && nextCandidateMtimeMs < visibleCutoff
}
function isAiVaultSession(session: AiVaultSession | null): session is AiVaultSession {
return Boolean(session)
}
@@ -0,0 +1,121 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AiVaultSession } from '../../shared/ai-vault-types'
import { CodexSessionCollection } from './codex-session-root-dedup'
import { canStopParsingSessions } from './session-scan-cutoff'
import { createAccumulator, finalizeSession, sessionSortTime } from './session-scanner-accumulator'
function session(time: number | string, overrides: Partial<AiVaultSession> = {}): AiVaultSession {
const parsed = finalizeSession(
createAccumulator({
agent: 'claude',
sessionId: 'session',
file: { path: '/sessions/session.jsonl', mtimeMs: 0, modifiedAt: new Date(0).toISOString() }
}),
'linux'
)
if (!parsed) {
throw new Error('Expected a session fixture')
}
return Object.freeze({
...parsed,
updatedAt: typeof time === 'number' ? new Date(time).toISOString() : time,
...overrides
})
}
function collection(rows: AiVaultSession[]): CodexSessionCollection {
const result = new CodexSessionCollection()
rows.forEach((row) => result.add(row))
return result
}
function sortedReference(rows: AiVaultSession[], limit: number, next: number | undefined): boolean {
if (rows.length < limit || typeof next !== 'number') {
return false
}
const cutoff = rows
.map(sessionSortTime)
.sort((left, right) => right - left)
.at(limit - 1)
return typeof cutoff === 'number' && next < cutoff
}
afterEach(() => vi.restoreAllMocks())
describe('canStopParsingSessions', () => {
it('counts strictly newer rows without sorting or mutating their order', () => {
const rows = [session(8), session(2), session(6), session(4)]
const sessions = collection(rows)
const sort = vi.spyOn(Array.prototype, 'sort')
expect(canStopParsingSessions(sessions, 2, 5)).toBe(true)
expect(canStopParsingSessions(sessions, 2, 6)).toBe(false)
expect(canStopParsingSessions(sessions, 4, 1)).toBe(true)
expect(canStopParsingSessions(sessions, 4, 2)).toBe(false)
expect(sort).not.toHaveBeenCalled()
expect([...sessions.values()]).toEqual(rows)
})
it('does not visit rows before the unique-session budget is met', () => {
const sessions = collection([session(5)])
const values = vi.spyOn(sessions, 'values')
expect(canStopParsingSessions(sessions, 2, 0)).toBe(false)
expect(canStopParsingSessions(sessions, Number.POSITIVE_INFINITY, 0)).toBe(false)
expect(canStopParsingSessions(sessions, 1, undefined)).toBe(false)
expect(values).not.toHaveBeenCalled()
})
it('recounts a preferred alias even when replacement lowers its timestamp', () => {
const alias = {
agent: 'codex' as const,
sessionId: 'same',
filePath: '/sessions/rollout-same.jsonl'
}
const sessions = collection([session(100, { ...alias, codexHome: '/custom' }), session(100)])
expect(canStopParsingSessions(sessions, 2, 50)).toBe(true)
const preferred = session(10, { ...alias, codexHome: null })
sessions.add(preferred)
expect(sessions.size).toBe(2)
expect(canStopParsingSessions(sessions, 2, 50)).toBe(false)
sessions.add(preferred)
expect(sessions.size).toBe(3)
expect(canStopParsingSessions(sessions, 3, 9)).toBe(true)
})
it('preserves the legacy sort result when a later timestamp is invalid', () => {
const rows = [session(0), session('invalid'), session(20)]
const sessions = collection(rows)
expect(canStopParsingSessions(sessions, 1, 10)).toBe(false)
for (const candidate of [rows, rows.toReversed(), [rows[2], rows[0], rows[1]]]) {
for (const limit of [1, 2, 3]) {
for (const next of [-1, 0, 10, 20]) {
expect(canStopParsingSessions(collection(candidate), limit, next)).toBe(
sortedReference(candidate, limit, next)
)
}
}
}
})
it('parses timestamps only once when an invalid date appears at the end', () => {
const sessions = collection([session(10), session(5), session('invalid')])
const parse = vi.spyOn(Date, 'parse')
canStopParsingSessions(sessions, 1, 0)
expect(parse).toHaveBeenCalledTimes(3)
})
it('uses the same nullish modified-time fallback and numeric limit semantics', () => {
const rows = [
session('', { updatedAt: null, modifiedAt: '1970-01-01T00:00:02+00:00' }),
session('-000001-01-01T00:00:00Z'),
session('+010000-01-01T00:00:00Z'),
session(0)
]
for (const limit of [0, -1, -5, 0.5, 1.5, Number.NaN, Infinity, -Infinity, 1, 2, 4, 5]) {
for (const next of [undefined, Number.NaN, -Infinity, Infinity, -1, 0, 1, 2000]) {
expect(canStopParsingSessions(collection(rows), limit, next)).toBe(
sortedReference(rows, limit, next)
)
}
}
})
})
+41
View File
@@ -0,0 +1,41 @@
import type { CodexSessionCollection } from './codex-session-root-dedup'
import { sessionSortTime } from './session-scanner-accumulator'
type ScanSessions = Pick<CodexSessionCollection, 'size' | 'values'>
function sortedCutoffIsNewer(
times: number[],
limit: number,
nextCandidateMtimeMs: number
): boolean {
const visibleCutoff = times.sort((left, right) => right - left).at(limit - 1)
return typeof visibleCutoff === 'number' && nextCandidateMtimeMs < visibleCutoff
}
export function canStopParsingSessions(
sessions: ScanSessions,
limit: number,
nextCandidateMtimeMs: number | undefined
): boolean {
if (sessions.size < limit || typeof nextCandidateMtimeMs !== 'number') {
return false
}
const times = Array.from(sessions.values(), sessionSortTime)
if (!Number.isInteger(limit) || limit <= 0) {
return sortedCutoffIsNewer(times, limit, nextCandidateMtimeMs)
}
// The top-N cutoff is newer exactly when N retained sessions beat the next mtime.
let newerCount = 0
for (const time of times) {
if (Number.isNaN(time)) {
// NaN makes the old comparator inconsistent; preserve its ordering verbatim.
return sortedCutoffIsNewer(times, limit, nextCandidateMtimeMs)
}
if (time > nextCandidateMtimeMs) {
newerCount += 1
}
}
// Check every timestamp before deciding: a later NaN requires the legacy sort.
return newerCount >= limit
}
@@ -0,0 +1,77 @@
import { mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform'
import { scanRemoteAiVaultSessions } from './remote-session-scanner'
import { MemoryRemoteProvider } from './remote-session-scanner-test-fixtures'
import { scanAiVaultSessions } from './session-scanner'
import { isolatedScanRoots, jsonLines } from './session-scanner-test-fixtures'
const tempRoots: string[] = []
afterEach(async () => {
vi.restoreAllMocks()
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
describe('session scanner cutoff', () => {
it.each(['native', 'remote'] as const)(
'%s does not sort timestamps at every post-limit candidate',
async (host) => {
const count = 128
const limit = count / 2
const provider = new MemoryRemoteProvider()
const root = await mkdtemp(join(tmpdir(), 'orca-session-cutoff-'))
tempRoots.push(root)
const roots = isolatedScanRoots(root)
await mkdir(roots.codexSessionsDir, { recursive: true })
for (let index = 0; index < count; index++) {
const name = `rollout-session-${index}.jsonl`
const content = jsonLines([
{ type: 'session_meta', payload: { id: `session-${index}`, cwd: '/repo/folder' } },
{
type: 'event_msg',
timestamp: new Date(index).toISOString(),
payload: { type: 'user_message', message: 'Check this session' }
}
])
const mtime = 10_000 - index
if (host === 'native') {
const filePath = join(roots.codexSessionsDir, name)
await writeFile(filePath, content)
await utimes(filePath, new Date(mtime), new Date(mtime))
} else {
provider.addFile(`/home/ada/.codex/sessions/${name}`, content, mtime)
}
}
let numericSorts = 0
const originalSort = Array.prototype.sort
vi.spyOn(Array.prototype, 'sort').mockImplementation(function (this: unknown[], compare) {
if (typeof this[0] === 'number') {
numericSorts++
}
return originalSort.call(this, compare)
})
const scan = () =>
host === 'native'
? scanAiVaultSessions({ ...roots, limit })
: scanRemoteAiVaultSessions({
provider,
remoteHome: '/home/ada',
hostPlatform: getRemoteHostPlatform('linux-x64'),
executionHostId: 'ssh:scan-cutoff',
limit
})
for (let pass = 0; pass < 2; pass++) {
numericSorts = 0
const result = await scan()
expect(result.issues).toEqual([])
expect(result.sessions.map((row) => row.sessionId)).toEqual(
Array.from({ length: limit }, (_, index) => `session-${count - index - 1}`)
)
expect(numericSorts).toBe(0)
}
}
)
})
+1 -17
View File
@@ -23,6 +23,7 @@ import {
type SessionParseStats
} from './session-scanner-parse-cache'
import { recordSessionScanIssue } from './session-scan-issues'
import { canStopParsingSessions } from './session-scan-cutoff'
import { discoverInScopeClaudeFiles } from './session-scanner-scope-discovery'
import { discoverAiVaultSessionSources } from './session-scanner-source-discovery'
import { cursorChatMetaRefusals, withCursorChatMetaScan } from './session-scanner-cursor-chat-meta'
@@ -296,20 +297,3 @@ function withSessionExecutionHost(
id: `${executionHostId}:${session.agent}:${session.sessionId}:${session.filePath}`
}
}
function canStopParsingSessions(
sessions: CodexSessionCollection,
limit: number,
nextCandidateMtimeMs: number | undefined
): boolean {
if (sessions.size < limit || typeof nextCandidateMtimeMs !== 'number') {
return false
}
const visibleCutoff = Array.from(sessions.values(), sessionSortTime)
.sort((left, right) => right - left)
.at(limit - 1)
// Transcript mtime is already our discovery bound and fallback sort key; older
// files cannot displace the current visible set once the cutoff is newer.
return typeof visibleCutoff === 'number' && nextCandidateMtimeMs < visibleCutoff
}