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:
1 parent
3dd3fa5d05
commit
e8927ebf78
3 files changed
+145
-32
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user