mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
chore(lint): parallelize more safe await-in-loop in server
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<import("node:fs").Dirent> = [];
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -372,35 +372,33 @@ async function listCodexCustomPrompts(): Promise<AgentSlashCommand[]> {
|
||||
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<AgentSlashCommand | null> => {
|
||||
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<unknown>;
|
||||
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<AgentPromptContentBlock, { type: "github_pr" | "github_issue" }>,
|
||||
),
|
||||
});
|
||||
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<AgentPromptContentBlock, { type: "github_pr" | "github_issue" }>,
|
||||
),
|
||||
};
|
||||
}
|
||||
return block;
|
||||
}),
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -4243,56 +4241,54 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
data?: Array<any>;
|
||||
};
|
||||
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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -244,43 +244,45 @@ export class ScheduleService {
|
||||
private async recoverInterruptedRuns(): Promise<void> {
|
||||
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<void> {
|
||||
|
||||
Reference in New Issue
Block a user