perf: optimize subagent transcript lookup with cache, meta-based resolution and startup index

This commit is contained in:
DoSun 2026-05-20 10:34:31 +08:00
parent c5a3142dfe
commit c63d8c60ce
4 changed files with 86 additions and 5 deletions

View File

@ -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
}

View File

@ -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<string>()
private _activeSubagents = new Map<string, SubagentInfo>()
private _subagentIndex = new Map<string, { transcriptPath: string; meta: Record<string, unknown> }>()
get subagentIndex(): ReadonlyMap<string, { transcriptPath: string; meta: Record<string, unknown> }> {
return this._subagentIndex
}
get tombstonedUuids(): ReadonlySet<string> {
return this._tombstonedUuids
@ -215,6 +224,28 @@ export class SessionHandle implements DisposableChildProcess {
}
}
async buildSubagentIndex(): Promise<void> {
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<string, unknown> = {}
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',

View File

@ -248,6 +248,7 @@ export class SessionManager {
try {
handle.spawn()
handle.buildSubagentIndex().catch(() => {})
} catch (err) {
this.sessions.delete(sessionId)
this.eventBus.unregisterSessionCwd(sessionId)

View File

@ -175,6 +175,23 @@ const SKIP_TYPES = new Set([
])
const pathCache = new Map<string, string | null>()
const subagentPathCache = new Map<string, string | null>()
type AgentMeta = {
parent_session_id?: string
agentType?: string
description?: string
prompt?: string
}
async function readAgentMeta(metaPath: string): Promise<AgentMeta | null> {
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<string | null> {
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<string | null> {
const fileName = `agent-${agentId}.jsonl`
const metaFileName = `agent-${agentId}.meta.json`
const tryDirect = async (projectDir: string): Promise<string | null> => {
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<string | null> => {
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()
}