perf: optimize subagent transcript lookup with cache, meta-based resolution and startup index
This commit is contained in:
parent
c5a3142dfe
commit
c63d8c60ce
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ export class SessionManager {
|
|||
|
||||
try {
|
||||
handle.spawn()
|
||||
handle.buildSubagentIndex().catch(() => {})
|
||||
} catch (err) {
|
||||
this.sessions.delete(sessionId)
|
||||
this.eventBus.unregisterSessionCwd(sessionId)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user