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 {
|
function findParentCwd(sessionManager: SessionManager, id: string): string | undefined {
|
||||||
for (const handle of sessionManager.getAllSessions()) {
|
for (const handle of sessionManager.getAllSessions()) {
|
||||||
|
if (handle.subagentIndex.has(id)) {
|
||||||
|
return handle.spawnCwd ?? handle.cwd
|
||||||
|
}
|
||||||
if (handle.activeSubagents.has(id)) {
|
if (handle.activeSubagents.has(id)) {
|
||||||
return handle.spawnCwd ?? handle.cwd
|
return handle.spawnCwd ?? handle.cwd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { type ChildProcess, spawn } from 'child_process'
|
import { type ChildProcess, spawn } from 'child_process'
|
||||||
import { createInterface } from 'readline'
|
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 { jsonParse, jsonStringify } from '../utils/slowOperations.js'
|
||||||
import { logError } from '../utils/log.js'
|
import { logError } from '../utils/log.js'
|
||||||
import type { EventBus } from './eventBus.js'
|
import type { EventBus } from './eventBus.js'
|
||||||
|
|
@ -10,10 +13,10 @@ import {
|
||||||
} from './childSpawn.js'
|
} from './childSpawn.js'
|
||||||
import { ControlChannel } from './sessionControlChannel.js'
|
import { ControlChannel } from './sessionControlChannel.js'
|
||||||
import {
|
import {
|
||||||
|
type SubagentInfo,
|
||||||
routeMessage,
|
routeMessage,
|
||||||
type StdoutMessage,
|
type StdoutMessage,
|
||||||
type MessageRouterCtx,
|
type MessageRouterCtx,
|
||||||
type SubagentInfo,
|
|
||||||
} from './sessionMessageRouter.js'
|
} from './sessionMessageRouter.js'
|
||||||
import type {
|
import type {
|
||||||
SessionBusyStatus,
|
SessionBusyStatus,
|
||||||
|
|
@ -24,6 +27,7 @@ import type {
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
import type { DisposableChildProcess } from '../utils/killOnDrop.js'
|
import type { DisposableChildProcess } from '../utils/killOnDrop.js'
|
||||||
import type { SessionMessage } from './transcriptReader.js'
|
import type { SessionMessage } from './transcriptReader.js'
|
||||||
|
import { getProjectDir, findProjectDir } from '../utils/sessionStoragePortable.js'
|
||||||
|
|
||||||
export type { InitData, PendingPermission, PendingQuestion }
|
export type { InitData, PendingPermission, PendingQuestion }
|
||||||
export { getScriptArgsForChild }
|
export { getScriptArgsForChild }
|
||||||
|
|
@ -88,6 +92,11 @@ export class SessionHandle implements DisposableChildProcess {
|
||||||
private _messageBuffer: SessionMessage[] = []
|
private _messageBuffer: SessionMessage[] = []
|
||||||
private _tombstonedUuids = new Set<string>()
|
private _tombstonedUuids = new Set<string>()
|
||||||
private _activeSubagents = new Map<string, SubagentInfo>()
|
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> {
|
get tombstonedUuids(): ReadonlySet<string> {
|
||||||
return this._tombstonedUuids
|
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 {
|
spawn(): void {
|
||||||
const printArgs = [
|
const printArgs = [
|
||||||
'--print',
|
'--print',
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,7 @@ export class SessionManager {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handle.spawn()
|
handle.spawn()
|
||||||
|
handle.buildSubagentIndex().catch(() => {})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.sessions.delete(sessionId)
|
this.sessions.delete(sessionId)
|
||||||
this.eventBus.unregisterSessionCwd(sessionId)
|
this.eventBus.unregisterSessionCwd(sessionId)
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,23 @@ const SKIP_TYPES = new Set([
|
||||||
])
|
])
|
||||||
|
|
||||||
const pathCache = new Map<string, string | null>()
|
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(
|
async function resolveTranscriptPath(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
|
|
@ -307,10 +324,40 @@ function readMessagesFromLines(
|
||||||
async function findSubagentTranscriptPath(
|
async function findSubagentTranscriptPath(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
cwd?: 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> {
|
): Promise<string | null> {
|
||||||
const fileName = `agent-${agentId}.jsonl`
|
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 searchProject = async (projectDir: string): Promise<string | null> => {
|
||||||
|
const directHit = await tryDirect(projectDir)
|
||||||
|
if (directHit) return directHit
|
||||||
|
|
||||||
let sessionDirs: string[]
|
let sessionDirs: string[]
|
||||||
try {
|
try {
|
||||||
sessionDirs = await readdir(projectDir)
|
sessionDirs = await readdir(projectDir)
|
||||||
|
|
@ -322,8 +369,7 @@ async function findSubagentTranscriptPath(
|
||||||
try {
|
try {
|
||||||
const s = await stat(direct)
|
const s = await stat(direct)
|
||||||
if (s.isFile() && s.size > 0) return direct
|
if (s.isFile() && s.size > 0) return direct
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
const nestedSubagentsDir = join(projectDir, sessionDir, 'subagents')
|
const nestedSubagentsDir = join(projectDir, sessionDir, 'subagents')
|
||||||
let subdirs: string[]
|
let subdirs: string[]
|
||||||
try {
|
try {
|
||||||
|
|
@ -336,8 +382,7 @@ async function findSubagentTranscriptPath(
|
||||||
try {
|
try {
|
||||||
const s = await stat(candidate)
|
const s = await stat(candidate)
|
||||||
if (s.isFile() && s.size > 0) return candidate
|
if (s.isFile() && s.size > 0) return candidate
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|
@ -705,4 +750,5 @@ export async function readSessionTasks(opts: {
|
||||||
|
|
||||||
export function clearPathCache(): void {
|
export function clearPathCache(): void {
|
||||||
pathCache.clear()
|
pathCache.clear()
|
||||||
|
subagentPathCache.clear()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user