diff --git a/config/scripts/session-scan-cutoff-benchmark.mjs b/config/scripts/session-scan-cutoff-benchmark.mjs
new file mode 100644
index 00000000000..4353754199e
--- /dev/null
+++ b/config/scripts/session-scan-cutoff-benchmark.mjs
@@ -0,0 +1,197 @@
+#!/usr/bin/env node
+// git show :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.')
diff --git a/src/main/ai-vault/remote-session-scanner.ts b/src/main/ai-vault/remote-session-scanner.ts
index b3fcc56ec9d..259c84d66ef 100644
--- a/src/main/ai-vault/remote-session-scanner.ts
+++ b/src/main/ai-vault/remote-session-scanner.ts
@@ -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)
}
diff --git a/src/main/ai-vault/session-scan-cutoff.test.ts b/src/main/ai-vault/session-scan-cutoff.test.ts
new file mode 100644
index 00000000000..53ddd0a019b
--- /dev/null
+++ b/src/main/ai-vault/session-scan-cutoff.test.ts
@@ -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 {
+ 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)
+ )
+ }
+ }
+ })
+})
diff --git a/src/main/ai-vault/session-scan-cutoff.ts b/src/main/ai-vault/session-scan-cutoff.ts
new file mode 100644
index 00000000000..e93d25bc985
--- /dev/null
+++ b/src/main/ai-vault/session-scan-cutoff.ts
@@ -0,0 +1,41 @@
+import type { CodexSessionCollection } from './codex-session-root-dedup'
+import { sessionSortTime } from './session-scanner-accumulator'
+
+type ScanSessions = Pick
+
+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
+}
diff --git a/src/main/ai-vault/session-scanner-cutoff.test.ts b/src/main/ai-vault/session-scanner-cutoff.test.ts
new file mode 100644
index 00000000000..76488ddabbe
--- /dev/null
+++ b/src/main/ai-vault/session-scanner-cutoff.test.ts
@@ -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)
+ }
+ }
+ )
+})
diff --git a/src/main/ai-vault/session-scanner.ts b/src/main/ai-vault/session-scanner.ts
index 9461fef6525..94c69258d59 100644
--- a/src/main/ai-vault/session-scanner.ts
+++ b/src/main/ai-vault/session-scanner.ts
@@ -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
-}