perf(extension): cache parsed session files by mtime and size

GET /api/sessions (and every session lookup) re-read and JSON.parsed every
.jsonl session file synchronously on each request, blocking the agent's
event loop. Parsed metadata is now cached per path (bounded to 500 entries)
and invalidated when mtime or size changes.
This commit is contained in:
evandro committed 2026-10-10 04:23:16 -03:00
1 parent 3dd3fa5d05
commit e8927ebf78
3 files changed
+145 -32

No files matched your search

+61 -32
View File
@@ -21,6 +21,7 @@ import {
type StoredSttSettings,
type StoredTtsSettings,
} from './piagentui-settings-store.js'
import { createStatCache } from './piagentui-stat-cache.js'
import { applySttSettingsUpdate, applyTtsSettingsUpdate, isTtsResponseFormat } from './piagentui-speech-settings.js'
import {
buildExtensionCatalog,
@@ -518,48 +519,76 @@ export default function (pi: ExtensionAPI) {
.slice(0, 120)
}
type ParsedSessionFile = {
id: string
title: string
cwd: string | undefined
headerTimestamp: string | undefined
messageCount: number
}
// GET /api/sessions parses every session file; re-parse only files whose mtime/size changed.
const sessionFileCache = createStatCache<ParsedSessionFile | null>()
function parseSessionFile(filePath: string): ParsedSessionFile | null {
const content = fs.readFileSync(filePath, 'utf8')
const lines = content.trim().split('\n').filter(Boolean)
if (lines.length === 0) return null
const header = JSON.parse(lines[0])
if (header.type !== 'session' || !header.id) return null
let sessionInfoTitle = ''
let firstUserMessage = ''
let messageCount = 0
for (const line of lines.slice(1)) {
try {
const entry = JSON.parse(line)
if (entry.type === 'session_info') {
sessionInfoTitle = normalizeSessionTitle(entry.name || '')
} else if (entry.type === 'message') {
messageCount++
if (!firstUserMessage && entry.message?.role === 'user') {
firstUserMessage = normalizeSessionTitle(getMessageText(entry.message))
}
}
} catch {
// ignore malformed entries
}
}
return {
id: header.id,
title: sessionInfoTitle || firstUserMessage || getFallbackSessionTitle(header.id),
cwd: header.cwd,
headerTimestamp: header.timestamp,
messageCount,
}
}
function readSessionMetadata(filePath: string): any | null {
try {
const content = fs.readFileSync(filePath, 'utf8')
const lines = content.trim().split('\n').filter(Boolean)
if (lines.length === 0) return null
const header = JSON.parse(lines[0])
if (header.type !== 'session' || !header.id) return null
let sessionInfoTitle = ''
let firstUserMessage = ''
let messageCount = 0
for (const line of lines.slice(1)) {
try {
const entry = JSON.parse(line)
if (entry.type === 'session_info') {
sessionInfoTitle = normalizeSessionTitle(entry.name || '')
} else if (entry.type === 'message') {
messageCount++
if (!firstUserMessage && entry.message?.role === 'user') {
firstUserMessage = normalizeSessionTitle(getMessageText(entry.message))
}
}
} catch {
// ignore malformed entries
}
const stats = fs.statSync(filePath)
let cached = sessionFileCache.get(filePath, stats)
if (!cached) {
cached = { value: parseSessionFile(filePath) }
sessionFileCache.set(filePath, stats, cached.value)
}
const stats = fs.statSync(filePath)
const createdTime = header.timestamp ? new Date(header.timestamp).getTime() : stats.birthtimeMs
const title = sessionInfoTitle || firstUserMessage || getFallbackSessionTitle(header.id)
const parsed = cached.value
if (!parsed) return null
const createdTime = parsed.headerTimestamp ? new Date(parsed.headerTimestamp).getTime() : stats.birthtimeMs
return {
id: header.id,
title,
directory: header.cwd || latestCtx?.cwd || process.cwd(),
id: parsed.id,
title: parsed.title,
directory: parsed.cwd || latestCtx?.cwd || process.cwd(),
time: {
created: Number.isNaN(createdTime) ? stats.birthtimeMs : createdTime,
updated: stats.mtimeMs,
},
summary: { deletions: 0, files: 0, additions: messageCount },
summary: { deletions: 0, files: 0, additions: parsed.messageCount },
}
} catch {
return null