mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
perf(ai-vault): count recent sessions for scan cutoffs
This commit is contained in:
@@ -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.')
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user