diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index a383bb7ed..b00d76a52 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -545,20 +545,23 @@ export class AgentManager { return client.listPersistedAgents({ limit: options.limit }); } - const descriptors: PersistedAgentDescriptor[] = []; - for (const [provider, client] of this.clients.entries()) { - if (!client.listPersistedAgents) { - continue; - } - try { - const entries = await client.listPersistedAgents({ - limit: options?.limit, - }); - descriptors.push(...entries); - } catch (error) { - this.logger.warn({ err: error, provider }, "Failed to list persisted agents for provider"); - } - } + const providerEntries = Array.from(this.clients.entries()).filter( + ([, client]) => !!client.listPersistedAgents, + ); + const descriptorLists = await Promise.all( + providerEntries.map(async ([provider, client]) => { + try { + return await client.listPersistedAgents!({ limit: options?.limit }); + } catch (error) { + this.logger.warn( + { err: error, provider }, + "Failed to list persisted agents for provider", + ); + return []; + } + }), + ); + const descriptors: PersistedAgentDescriptor[] = descriptorLists.flat(); const limit = options?.limit ?? 20; return descriptors diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index 731394128..ee8a7ee87 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -274,45 +274,42 @@ export class AgentStorage { throw error; } - for (const entry of entries) { - if (entry.isFile() && entry.name.endsWith(".json")) { - const rootPath = path.join(this.baseDir, entry.name); - const rootRecord = await this.readRecordFile(rootPath); - if (!rootRecord) { - continue; - } - records.push(rootRecord); - this.cache.set(rootRecord.id, rootRecord); - this.pathById.set(rootRecord.id, rootPath); - this.addIndexedPath(rootRecord.id, rootPath); - continue; - } + const rootRecordPaths = entries + .filter((entry) => entry.isFile() && entry.name.endsWith(".json")) + .map((entry) => path.join(this.baseDir, entry.name)); - if (!entry.isDirectory()) { - continue; - } - const projectDir = path.join(this.baseDir, entry.name); - let files: Array = []; - try { - files = await fs.readdir(projectDir, { withFileTypes: true }); - } catch { - continue; - } + const projectDirs = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(this.baseDir, entry.name)); - for (const file of files) { - if (!file.isFile() || !file.name.endsWith(".json")) { - continue; + const projectFileLists = await Promise.all( + projectDirs.map(async (projectDir) => { + try { + const files = await fs.readdir(projectDir, { withFileTypes: true }); + return files + .filter((file) => file.isFile() && file.name.endsWith(".json")) + .map((file) => path.join(projectDir, file.name)); + } catch { + return []; } - const filePath = path.join(projectDir, file.name); + }), + ); + + const allFilePaths = [...rootRecordPaths, ...projectFileLists.flat()]; + const loaded = await Promise.all( + allFilePaths.map(async (filePath) => { const record = await this.readRecordFile(filePath); - if (!record) { - continue; - } - records.push(record); - this.cache.set(record.id, record); - this.pathById.set(record.id, filePath); - this.addIndexedPath(record.id, filePath); - } + return record ? { record, filePath } : null; + }), + ); + + for (const item of loaded) { + if (!item) continue; + const { record, filePath } = item; + records.push(record); + this.cache.set(record.id, record); + this.pathById.set(record.id, filePath); + this.addIndexedPath(record.id, filePath); } return records; diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 3c0e15877..0f6662ada 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -4285,37 +4285,35 @@ async function collectRecentClaudeSessions( } catch { return []; } - const candidates: ClaudeSessionCandidate[] = []; - for (const dirName of projectDirs) { - const projectPath = path.join(root, dirName); - let stats: fs.Stats; - try { - stats = await fsPromises.stat(projectPath); - } catch { - continue; - } - if (!stats.isDirectory()) { - continue; - } - let files: string[]; - try { - files = await fsPromises.readdir(projectPath); - } catch { - continue; - } - for (const file of files) { - if (!file.endsWith(".jsonl")) { - continue; + const projectFileLists = await Promise.all( + projectDirs.map(async (dirName) => { + const projectPath = path.join(root, dirName); + try { + const stats = await fsPromises.stat(projectPath); + if (!stats.isDirectory()) return { projectPath, files: [] as string[] }; + const files = await fsPromises.readdir(projectPath); + return { projectPath, files }; + } catch { + return { projectPath, files: [] as string[] }; } - const fullPath = path.join(projectPath, file); + }), + ); + const fileEntries = projectFileLists.flatMap(({ projectPath, files }) => + files.filter((f) => f.endsWith(".jsonl")).map((f) => path.join(projectPath, f)), + ); + const statResults = await Promise.all( + fileEntries.map(async (fullPath) => { try { const fileStats = await fsPromises.stat(fullPath); - candidates.push({ path: fullPath, mtime: fileStats.mtime }); + return { path: fullPath, mtime: fileStats.mtime }; } catch { - // ignore stat errors for individual files + return null; } - } - } + }), + ); + const candidates: ClaudeSessionCandidate[] = statResults.filter( + (entry): entry is ClaudeSessionCandidate => entry !== null, + ); return candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()).slice(0, limit); } diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index fc6982c89..5f63ad573 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -372,35 +372,33 @@ async function listCodexCustomPrompts(): Promise { return []; } - const commands: AgentSlashCommand[] = []; - for (const entry of entries) { - if (!entry.isFile()) { - continue; - } - if (!entry.name.endsWith(".md")) { - continue; - } - const name = entry.name.slice(0, -".md".length); - if (!name) { - continue; - } - const fullPath = path.join(promptsDir, entry.name); - let content: string; - try { - content = await fs.readFile(fullPath, "utf8"); - } catch { - continue; - } - const parsed = parseFrontMatter(content); - const description = parsed.frontMatter["description"] ?? "Custom prompt"; - const argumentHint = - parsed.frontMatter["argument-hint"] ?? parsed.frontMatter["argument_hint"] ?? ""; - commands.push({ - name: `prompts:${name}`, - description, - argumentHint, - }); - } + const mdEntries = entries.filter( + (entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name.slice(0, -".md".length), + ); + const parsedCommands = await Promise.all( + mdEntries.map(async (entry): Promise => { + const name = entry.name.slice(0, -".md".length); + const fullPath = path.join(promptsDir, entry.name); + let content: string; + try { + content = await fs.readFile(fullPath, "utf8"); + } catch { + return null; + } + const parsed = parseFrontMatter(content); + const description = parsed.frontMatter["description"] ?? "Custom prompt"; + const argumentHint = + parsed.frontMatter["argument-hint"] ?? parsed.frontMatter["argument_hint"] ?? ""; + return { + name: `prompts:${name}`, + description, + argumentHint, + }; + }), + ); + const commands: AgentSlashCommand[] = parsedCommands.filter( + (cmd): cmd is AgentSlashCommand => cmd !== null, + ); return commands.sort((a, b) => a.name.localeCompare(b.name)); } @@ -431,18 +429,20 @@ async function listCodexSkills( continue; } - for (const entry of entries) { - if (!entry.isDirectory() && !entry.isSymbolicLink()) { - continue; - } - const skillDir = path.join(dir, entry.name); - const skillPath = path.join(skillDir, "SKILL.md"); - let content: string; - try { - content = await fs.readFile(skillPath, "utf8"); - } catch { - continue; - } + const dirEntries = entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink()); + const skillContents = await Promise.all( + dirEntries.map(async (entry) => { + const skillDir = path.join(dir, entry.name); + const skillPath = path.join(skillDir, "SKILL.md"); + try { + return await fs.readFile(skillPath, "utf8"); + } catch { + return null; + } + }), + ); + for (const content of skillContents) { + if (!content) continue; const { frontMatter } = parseFrontMatter(content); const name = frontMatter["name"]; const description = frontMatter["description"]; @@ -2396,42 +2396,40 @@ export async function codexAppServerTurnInputFromPrompt( } const blocks = prompt as Array; - const output: unknown[] = []; - for (const block of blocks) { - if (!block || typeof block !== "object") { - output.push(block); - continue; - } - const record = block as { type?: unknown; mimeType?: unknown; data?: unknown }; - if ( - record.type === "image" && - typeof record.mimeType === "string" && - typeof record.data === "string" - ) { - try { - const filePath = await writeImageAttachment(record.mimeType, record.data); - output.push({ type: "localImage", path: filePath }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - logger.warn({ message }, "Failed to write Codex image attachment"); - output.push({ - type: "text", - text: `User attached image (failed to write temp file): ${message}`, - }); + const output = await Promise.all( + blocks.map(async (block) => { + if (!block || typeof block !== "object") { + return block; } - continue; - } - if (record.type === "github_pr" || record.type === "github_issue") { - output.push({ - type: "text", - text: renderPromptAttachmentAsText( - record as Extract, - ), - }); - continue; - } - output.push(block); - } + const record = block as { type?: unknown; mimeType?: unknown; data?: unknown }; + if ( + record.type === "image" && + typeof record.mimeType === "string" && + typeof record.data === "string" + ) { + try { + const filePath = await writeImageAttachment(record.mimeType, record.data); + return { type: "localImage", path: filePath }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.warn({ message }, "Failed to write Codex image attachment"); + return { + type: "text", + text: `User attached image (failed to write temp file): ${message}`, + }; + } + } + if (record.type === "github_pr" || record.type === "github_issue") { + return { + type: "text", + text: renderPromptAttachmentAsText( + record as Extract, + ), + }; + } + return block; + }), + ); return output; } @@ -4243,56 +4241,54 @@ export class CodexAppServerAgentClient implements AgentClient { data?: Array; }; const threads = Array.isArray(response?.data) ? response.data : []; - const descriptors: PersistedAgentDescriptor[] = []; - - for (const thread of threads.slice(0, limit)) { - const threadId = thread.id; - const cwd = thread.cwd ?? process.cwd(); - const title = thread.preview ?? null; - let timeline: AgentTimelineItem[] = []; - try { - const rolloutTimeline = await loadCodexPersistedTimeline( - threadId, - undefined, - this.logger, - ); - const read = (await client.request("thread/read", { - threadId, - includeTurns: true, - })) as { thread?: { turns?: Array<{ items?: any[] }> } }; - const turns = read.thread?.turns ?? []; - const itemsFromThreadRead: AgentTimelineItem[] = []; - for (const turn of turns) { - for (const item of turn.items ?? []) { - const timelineItem = threadItemToTimeline(item, { cwd }); - if (timelineItem) itemsFromThreadRead.push(timelineItem); + const descriptors: PersistedAgentDescriptor[] = await Promise.all( + threads.slice(0, limit).map(async (thread) => { + const threadId = thread.id; + const cwd = thread.cwd ?? process.cwd(); + const title = thread.preview ?? null; + let timeline: AgentTimelineItem[] = []; + try { + const [rolloutTimeline, read] = await Promise.all([ + loadCodexPersistedTimeline(threadId, undefined, this.logger), + client.request("thread/read", { + threadId, + includeTurns: true, + }) as Promise<{ thread?: { turns?: Array<{ items?: any[] }> } }>, + ]); + const turns = read.thread?.turns ?? []; + const itemsFromThreadRead: AgentTimelineItem[] = []; + for (const turn of turns) { + for (const item of turn.items ?? []) { + const timelineItem = threadItemToTimeline(item, { cwd }); + if (timelineItem) itemsFromThreadRead.push(timelineItem); + } } + timeline = rolloutTimeline.length > 0 ? rolloutTimeline : itemsFromThreadRead; + } catch { + timeline = []; } - timeline = rolloutTimeline.length > 0 ? rolloutTimeline : itemsFromThreadRead; - } catch { - timeline = []; - } - descriptors.push({ - provider: CODEX_PROVIDER, - sessionId: threadId, - cwd, - title, - lastActivityAt: new Date((thread.updatedAt ?? thread.createdAt ?? 0) * 1000), - persistence: { + return { provider: CODEX_PROVIDER, sessionId: threadId, - nativeHandle: threadId, - metadata: { + cwd, + title, + lastActivityAt: new Date((thread.updatedAt ?? thread.createdAt ?? 0) * 1000), + persistence: { provider: CODEX_PROVIDER, - cwd, - title, - threadId, + sessionId: threadId, + nativeHandle: threadId, + metadata: { + provider: CODEX_PROVIDER, + cwd, + title, + threadId, + }, }, - }, - timeline, - }); - } + timeline, + }; + }), + ); return descriptors; } finally { diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index b21cc1386..31aebbbf7 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -905,9 +905,7 @@ export class OpenCodeServerManager { ...(this.currentServer ? [this.currentServer] : []), ...Array.from(this.retiredServers), ]; - for (const server of servers) { - await this.killServer(server); - } + await Promise.all(servers.map((server) => this.killServer(server))); this.currentServer = null; this.retiredServers.clear(); } diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index 8832fbf4a..b91e78d81 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -244,43 +244,45 @@ export class ScheduleService { private async recoverInterruptedRuns(): Promise { const schedules = await this.store.list(); const now = this.now(); - for (const schedule of schedules) { - let updated = { ...schedule }; - let dirty = false; + await Promise.all( + schedules.map(async (schedule) => { + let updated = { ...schedule }; + let dirty = false; - // Mark any in-flight runs as failed - const runningIndex = updated.runs.findIndex((run) => run.status === "running"); - if (runningIndex !== -1) { - const runs = [...updated.runs]; - runs[runningIndex] = { - ...runs[runningIndex], - status: "failed", - endedAt: now.toISOString(), - error: "Daemon restarted before the scheduled run completed", - }; - updated = { ...updated, runs }; - dirty = true; - } - - // Advance stale nextRunAt for active schedules - if ( - updated.status === "active" && - updated.nextRunAt && - new Date(updated.nextRunAt).getTime() <= now.getTime() - ) { - let nextRunAt = computeNextRunAt(updated.cadence, new Date(updated.nextRunAt)); - while (nextRunAt.getTime() <= now.getTime()) { - nextRunAt = computeNextRunAt(updated.cadence, nextRunAt); + // Mark any in-flight runs as failed + const runningIndex = updated.runs.findIndex((run) => run.status === "running"); + if (runningIndex !== -1) { + const runs = [...updated.runs]; + runs[runningIndex] = { + ...runs[runningIndex], + status: "failed", + endedAt: now.toISOString(), + error: "Daemon restarted before the scheduled run completed", + }; + updated = { ...updated, runs }; + dirty = true; } - updated = { ...updated, nextRunAt: nextRunAt.toISOString() }; - dirty = true; - } - if (dirty) { - updated = { ...updated, updatedAt: now.toISOString() }; - await this.store.put(updated); - } - } + // Advance stale nextRunAt for active schedules + if ( + updated.status === "active" && + updated.nextRunAt && + new Date(updated.nextRunAt).getTime() <= now.getTime() + ) { + let nextRunAt = computeNextRunAt(updated.cadence, new Date(updated.nextRunAt)); + while (nextRunAt.getTime() <= now.getTime()) { + nextRunAt = computeNextRunAt(updated.cadence, nextRunAt); + } + updated = { ...updated, nextRunAt: nextRunAt.toISOString() }; + dirty = true; + } + + if (dirty) { + updated = { ...updated, updatedAt: now.toISOString() }; + await this.store.put(updated); + } + }), + ); } private async runSchedule(schedule: StoredSchedule, now: Date): Promise {