Skip to content

Commit b144dd0

Browse files
author
Hex
committed
fix: cache 60MB sessions.json parse + clear threads on workspace switch
Root cause: /api/threads called getGatewayActivityMap() which read and parsed the 60MB sessions.json on every request — taking 10+ seconds and pegging CPU at 110%. Fixes: - Cache the parsed activity map with 10s TTL + mtime check (skip re-parse if file hasn't changed) - First request: 183ms, subsequent: 2ms - Client: clear thread list immediately on workspace switch so old workspace's threads don't persist while new ones load
1 parent 25dbcf4 commit b144dd0

File tree

2 files changed

+26
-1
lines changed

2 files changed

+26
-1
lines changed

packages/client/src/features/threads/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ function readThreadFromHash(): string {
2828

2929
export function fetchThreadsForOrg(orgId?: string): void {
3030
const id = orgId ?? activeOrgIdForThreads()
31+
// Clear stale threads immediately so the UI doesn't show the previous workspace's threads
32+
setThreads([])
3133
const url = id ? `/api/threads?orgId=${encodeURIComponent(id)}` : '/api/threads'
3234
fetch(url)
3335
.then((r) => r.json())

packages/server/src/threads/parse-gateway-sessions.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,34 @@ export function mergeWithLocal(sessions: ParsedSession[], localThreads: any[]):
5959
})
6060
}
6161

62+
// Cache the gateway activity map — 60MB sessions.json is too expensive to read on every request
63+
let cachedMap: Map<string, { lastActivity: number; status?: string }> | null = null
64+
let cachedMapTime = 0
65+
const CACHE_TTL = 10_000 // 10s — fresh enough for UI, avoids re-parsing 60MB
66+
6267
/** Read gateway sessions.json and return a map of shortKey → {lastActivity, status} */
6368
export async function getGatewayActivityMap(): Promise<Map<string, { lastActivity: number; status?: string }>> {
69+
const now = Date.now()
70+
if (cachedMap && now - cachedMapTime < CACHE_TTL) return cachedMap
71+
6472
const map = new Map<string, { lastActivity: number; status?: string }>()
6573
try {
6674
const { readFile } = await import('fs/promises')
6775
const { join } = await import('path')
76+
const { statSync } = await import('fs')
6877
const sessionsPath = join(process.env.HOME || '', '.openclaw/agents/main/sessions/sessions.json')
78+
79+
// Quick mtime check — skip re-parse if file hasn't changed
80+
try {
81+
const stat = statSync(sessionsPath)
82+
if (cachedMap && stat.mtimeMs <= cachedMapTime) {
83+
cachedMapTime = now
84+
return cachedMap
85+
}
86+
} catch {
87+
/* ignore */
88+
}
89+
6990
const raw = await readFile(sessionsPath, 'utf-8')
7091
const data = JSON.parse(raw) as Record<string, any>
7192
for (const [fullKey, meta] of Object.entries(data)) {
@@ -75,8 +96,10 @@ export async function getGatewayActivityMap(): Promise<Map<string, { lastActivit
7596
map.set(parsed.shortKey, { lastActivity: parsed.lastActivity, status })
7697
}
7798
}
99+
cachedMap = map
100+
cachedMapTime = now
78101
} catch {
79102
/* ignore read errors */
80103
}
81-
return map
104+
return cachedMap ?? map
82105
}

0 commit comments

Comments
 (0)