From c63d8c60cef1cb154512d0310fe3f45548b4f1ef Mon Sep 17 00:00:00 2001 From: DoSun Date: Wed, 20 May 2026 10:34:31 +0800 Subject: [PATCH] perf: optimize subagent transcript lookup with cache, meta-based resolution and startup index --- src/server/routes/message.ts | 3 ++ src/server/sessionHandle.ts | 33 ++++++++++++++++++++- src/server/sessionManager.ts | 1 + src/server/transcriptReader.ts | 54 +++++++++++++++++++++++++++++++--- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/server/routes/message.ts b/src/server/routes/message.ts index eb40506d1..d195ef2c4 100644 --- a/src/server/routes/message.ts +++ b/src/server/routes/message.ts @@ -15,6 +15,9 @@ import { getSubagentProgress } from '../sessionMessageRouter.js' function findParentCwd(sessionManager: SessionManager, id: string): string | undefined { for (const handle of sessionManager.getAllSessions()) { + if (handle.subagentIndex.has(id)) { + return handle.spawnCwd ?? handle.cwd + } if (handle.activeSubagents.has(id)) { return handle.spawnCwd ?? handle.cwd } diff --git a/src/server/sessionHandle.ts b/src/server/sessionHandle.ts index eae21cd82..9e553a0e3 100644 --- a/src/server/sessionHandle.ts +++ b/src/server/sessionHandle.ts @@ -1,5 +1,8 @@ import { type ChildProcess, spawn } from 'child_process' import { createInterface } from 'readline' +import { readdir, stat, readFile } from 'fs/promises' +import { existsSync } from 'fs' +import { join } from 'path' import { jsonParse, jsonStringify } from '../utils/slowOperations.js' import { logError } from '../utils/log.js' import type { EventBus } from './eventBus.js' @@ -10,10 +13,10 @@ import { } from './childSpawn.js' import { ControlChannel } from './sessionControlChannel.js' import { + type SubagentInfo, routeMessage, type StdoutMessage, type MessageRouterCtx, - type SubagentInfo, } from './sessionMessageRouter.js' import type { SessionBusyStatus, @@ -24,6 +27,7 @@ import type { } from './types.js' import type { DisposableChildProcess } from '../utils/killOnDrop.js' import type { SessionMessage } from './transcriptReader.js' +import { getProjectDir, findProjectDir } from '../utils/sessionStoragePortable.js' export type { InitData, PendingPermission, PendingQuestion } export { getScriptArgsForChild } @@ -88,6 +92,11 @@ export class SessionHandle implements DisposableChildProcess { private _messageBuffer: SessionMessage[] = [] private _tombstonedUuids = new Set() private _activeSubagents = new Map() + private _subagentIndex = new Map }>() + + get subagentIndex(): ReadonlyMap }> { + return this._subagentIndex + } get tombstonedUuids(): ReadonlySet { return this._tombstonedUuids @@ -215,6 +224,28 @@ export class SessionHandle implements DisposableChildProcess { } } + async buildSubagentIndex(): Promise { + const projectDir = getProjectDir(this.cwd) + const sessionDir = join(projectDir, this.sessionId, 'subagents') + if (!existsSync(sessionDir)) return + + try { + const entries = await readdir(sessionDir) + for (const entry of entries) { + if (!entry.startsWith('agent-') || !entry.endsWith('.jsonl')) continue + const agentId = entry.slice('agent-'.length, -'.jsonl'.length) + const transcriptPath = join(sessionDir, entry) + const metaPath = transcriptPath.replace(/\.jsonl$/, '.meta.json') + let meta: Record = {} + try { + const raw = await readFile(metaPath, 'utf-8') + meta = JSON.parse(raw) + } catch {} + this._subagentIndex.set(agentId, { transcriptPath, meta }) + } + } catch {} + } + spawn(): void { const printArgs = [ '--print', diff --git a/src/server/sessionManager.ts b/src/server/sessionManager.ts index 7e930d730..ab344e714 100644 --- a/src/server/sessionManager.ts +++ b/src/server/sessionManager.ts @@ -248,6 +248,7 @@ export class SessionManager { try { handle.spawn() + handle.buildSubagentIndex().catch(() => {}) } catch (err) { this.sessions.delete(sessionId) this.eventBus.unregisterSessionCwd(sessionId) diff --git a/src/server/transcriptReader.ts b/src/server/transcriptReader.ts index bd1dff5a0..af566db57 100644 --- a/src/server/transcriptReader.ts +++ b/src/server/transcriptReader.ts @@ -175,6 +175,23 @@ const SKIP_TYPES = new Set([ ]) const pathCache = new Map() +const subagentPathCache = new Map() + +type AgentMeta = { + parent_session_id?: string + agentType?: string + description?: string + prompt?: string +} + +async function readAgentMeta(metaPath: string): Promise { + try { + const raw = await readFile(metaPath, 'utf-8') + return JSON.parse(raw) as AgentMeta + } catch { + return null + } +} async function resolveTranscriptPath( sessionId: string, @@ -307,10 +324,40 @@ function readMessagesFromLines( async function findSubagentTranscriptPath( agentId: string, cwd?: string, +): Promise { + const cacheKey = `${agentId}:${cwd ?? ''}` + const cached = subagentPathCache.get(cacheKey) + if (cached !== undefined) return cached + + const result = await findSubagentTranscriptPathUncached(agentId, cwd) + if (result) subagentPathCache.set(cacheKey, result) + return result +} + +async function findSubagentTranscriptPathUncached( + agentId: string, + cwd?: string, ): Promise { const fileName = `agent-${agentId}.jsonl` + const metaFileName = `agent-${agentId}.meta.json` + + const tryDirect = async (projectDir: string): Promise => { + const metaPath = join(projectDir, metaFileName) + const meta = await readAgentMeta(metaPath) + if (meta?.parent_session_id) { + const direct = join(projectDir, meta.parent_session_id, 'subagents', fileName) + if (existsSync(direct)) { + const s = await stat(direct) + if (s.isFile() && s.size > 0) return direct + } + } + return null + } const searchProject = async (projectDir: string): Promise => { + const directHit = await tryDirect(projectDir) + if (directHit) return directHit + let sessionDirs: string[] try { sessionDirs = await readdir(projectDir) @@ -322,8 +369,7 @@ async function findSubagentTranscriptPath( try { const s = await stat(direct) if (s.isFile() && s.size > 0) return direct - } catch { - } + } catch {} const nestedSubagentsDir = join(projectDir, sessionDir, 'subagents') let subdirs: string[] try { @@ -336,8 +382,7 @@ async function findSubagentTranscriptPath( try { const s = await stat(candidate) if (s.isFile() && s.size > 0) return candidate - } catch { - } + } catch {} } } return null @@ -705,4 +750,5 @@ export async function readSessionTasks(opts: { export function clearPathCache(): void { pathCache.clear() + subagentPathCache.clear() }