Update files
This commit is contained in:
@@ -192,6 +192,10 @@ export interface ProjectGroup {
|
|||||||
projectKey: string;
|
projectKey: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
agents: AggregatedAgent[];
|
agents: AggregatedAgent[];
|
||||||
|
/** Number of truly active agents (running or requires attention) */
|
||||||
|
activeCount: number;
|
||||||
|
/** Total agents before any limit was applied */
|
||||||
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateGroup {
|
export interface DateGroup {
|
||||||
@@ -204,7 +208,7 @@ export interface GroupedAgents {
|
|||||||
inactiveGroups: DateGroup[];
|
inactiveGroups: DateGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTIVE_GRACE_PERIOD_MS = 15 * 60 * 1000; // 15 minutes
|
const ACTIVE_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
interface GroupAgentsOptions {
|
interface GroupAgentsOptions {
|
||||||
/**
|
/**
|
||||||
@@ -214,9 +218,15 @@ interface GroupAgentsOptions {
|
|||||||
getRemoteUrl?: (agent: AggregatedAgent) => string | null;
|
getRemoteUrl?: (agent: AggregatedAgent) => string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_INACTIVE_PER_PROJECT = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups agents into active (by project) and inactive (by date) sections.
|
* Groups agents into active (by project) and inactive (by date) sections.
|
||||||
* Active = running, requires attention, or had activity within the last 5 minutes.
|
* Active = running, requires attention, or had activity within the grace period (24 hours).
|
||||||
|
*
|
||||||
|
* Within each project group:
|
||||||
|
* - All truly active agents (running/requires attention) are always shown
|
||||||
|
* - Recently active (within grace period but not running) are limited to MAX_INACTIVE_PER_PROJECT
|
||||||
*/
|
*/
|
||||||
export function groupAgents(
|
export function groupAgents(
|
||||||
agents: AggregatedAgent[],
|
agents: AggregatedAgent[],
|
||||||
@@ -240,28 +250,61 @@ export function groupAgents(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group active agents by project
|
// Group active agents by project, tracking truly active vs recently active
|
||||||
const projectMap = new Map<string, AggregatedAgent[]>();
|
const projectMap = new Map<
|
||||||
|
string,
|
||||||
|
{ trulyActive: AggregatedAgent[]; recentlyActive: AggregatedAgent[] }
|
||||||
|
>();
|
||||||
for (const agent of activeAgents) {
|
for (const agent of activeAgents) {
|
||||||
const remoteKey = deriveRemoteProjectKey(
|
const remoteKey = deriveRemoteProjectKey(
|
||||||
options?.getRemoteUrl?.(agent) ?? null
|
options?.getRemoteUrl?.(agent) ?? null
|
||||||
);
|
);
|
||||||
const projectKey = remoteKey ?? deriveProjectKey(agent.cwd);
|
const projectKey = remoteKey ?? deriveProjectKey(agent.cwd);
|
||||||
const existing = projectMap.get(projectKey) || [];
|
const existing = projectMap.get(projectKey) || {
|
||||||
existing.push(agent);
|
trulyActive: [],
|
||||||
|
recentlyActive: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTrulyActive =
|
||||||
|
agent.status === "running" || agent.requiresAttention;
|
||||||
|
if (isTrulyActive) {
|
||||||
|
existing.trulyActive.push(agent);
|
||||||
|
} else {
|
||||||
|
existing.recentlyActive.push(agent);
|
||||||
|
}
|
||||||
|
|
||||||
projectMap.set(projectKey, existing);
|
projectMap.set(projectKey, existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort agents within each project by lastActivityAt (newest first)
|
// Build project groups with limits applied
|
||||||
const activeGroups: ProjectGroup[] = [];
|
const activeGroups: ProjectGroup[] = [];
|
||||||
for (const [projectKey, projectAgents] of projectMap) {
|
for (const [projectKey, { trulyActive, recentlyActive }] of projectMap) {
|
||||||
projectAgents.sort(
|
// Sort both arrays by lastActivityAt (newest first)
|
||||||
|
trulyActive.sort(
|
||||||
(a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime()
|
(a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime()
|
||||||
);
|
);
|
||||||
|
recentlyActive.sort(
|
||||||
|
(a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
// All truly active agents shown, limit recently active to MAX_INACTIVE_PER_PROJECT
|
||||||
|
const limitedRecentlyActive = recentlyActive.slice(
|
||||||
|
0,
|
||||||
|
MAX_INACTIVE_PER_PROJECT
|
||||||
|
);
|
||||||
|
const combinedAgents = [...trulyActive, ...limitedRecentlyActive];
|
||||||
|
|
||||||
|
// Re-sort combined list by lastActivityAt
|
||||||
|
combinedAgents.sort(
|
||||||
|
(a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime()
|
||||||
|
);
|
||||||
|
|
||||||
activeGroups.push({
|
activeGroups.push({
|
||||||
projectKey,
|
projectKey,
|
||||||
projectName: deriveProjectName(projectKey),
|
projectName: deriveProjectName(projectKey),
|
||||||
agents: projectAgents,
|
agents: combinedAgents,
|
||||||
|
activeCount: trulyActive.length,
|
||||||
|
totalCount: trulyActive.length + recentlyActive.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1046,6 +1046,10 @@ export class DaemonClientV2 {
|
|||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
const existing = this.checkoutStatusInFlight.get(agentId);
|
const existing = this.checkoutStatusInFlight.get(agentId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
this.logger.debug(
|
||||||
|
{ agentId, inFlightCount: this.checkoutStatusInFlight.size },
|
||||||
|
"getCheckoutStatus: returning existing in-flight request"
|
||||||
|
);
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1057,6 +1061,11 @@ export class DaemonClientV2 {
|
|||||||
requestId: resolvedRequestId,
|
requestId: resolvedRequestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
{ agentId, requestId: resolvedRequestId, waiterCount: this.waiters.size },
|
||||||
|
"getCheckoutStatus: creating new request"
|
||||||
|
);
|
||||||
|
|
||||||
const responsePromise = (async () => {
|
const responsePromise = (async () => {
|
||||||
const response = this.waitFor(
|
const response = this.waitFor(
|
||||||
(msg) => {
|
(msg) => {
|
||||||
@@ -1077,11 +1086,24 @@ export class DaemonClientV2 {
|
|||||||
|
|
||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
this.checkoutStatusInFlight.set(agentId, responsePromise);
|
this.checkoutStatusInFlight.set(agentId, responsePromise);
|
||||||
responsePromise.finally(() => {
|
responsePromise
|
||||||
if (this.checkoutStatusInFlight.get(agentId) === responsePromise) {
|
.then(() => {
|
||||||
this.checkoutStatusInFlight.delete(agentId);
|
this.logger.debug(
|
||||||
}
|
{ agentId, requestId: resolvedRequestId },
|
||||||
});
|
"getCheckoutStatus: request completed successfully"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.debug(
|
||||||
|
{ agentId, requestId: resolvedRequestId, error: err?.message },
|
||||||
|
"getCheckoutStatus: request failed"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (this.checkoutStatusInFlight.get(agentId) === responsePromise) {
|
||||||
|
this.checkoutStatusInFlight.delete(agentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return responsePromise;
|
return responsePromise;
|
||||||
@@ -2099,6 +2121,10 @@ export class DaemonClientV2 {
|
|||||||
this.lastErrorValue = reason.trim();
|
this.lastErrorValue = reason.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear all pending waiters since the connection was lost and responses
|
||||||
|
// from the previous connection will never arrive.
|
||||||
|
this.clearWaiters(new Error(reason ?? "Connection lost"));
|
||||||
|
|
||||||
this.updateConnectionState({
|
this.updateConnectionState({
|
||||||
status: "disconnected",
|
status: "disconnected",
|
||||||
...(reason ? { reason } : {}),
|
...(reason ? { reason } : {}),
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
/**
|
||||||
|
* Ad-hoc script to debug checkout_status_request timeouts.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts [agentId1] [agentId2]
|
||||||
|
*
|
||||||
|
* To test against a different daemon:
|
||||||
|
* PASEO_PORT=7777 npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WebSocket } from "ws";
|
||||||
|
import { DaemonClientV2 } from "../../client/daemon-client-v2.js";
|
||||||
|
|
||||||
|
// Patch WebSocket to log all messages
|
||||||
|
const OriginalWebSocket = WebSocket;
|
||||||
|
class LoggingWebSocket extends OriginalWebSocket {
|
||||||
|
constructor(url: string, ...args: any[]) {
|
||||||
|
super(url, ...args);
|
||||||
|
console.log(`[WS] Connecting to ${url}`);
|
||||||
|
this.on("open", () => console.log("[WS] Connection opened"));
|
||||||
|
this.on("close", (code, reason) => console.log(`[WS] Connection closed: ${code} ${reason}`));
|
||||||
|
this.on("error", (err) => console.log(`[WS] Error: ${err}`));
|
||||||
|
this.on("message", (data) => {
|
||||||
|
const str = data.toString().slice(0, 200);
|
||||||
|
console.log(`[WS] Message received (${data.toString().length} bytes): ${str}...`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PASEO_HOME = process.env.PASEO_HOME ?? "/Users/moboudra/.paseo";
|
||||||
|
const PASEO_PORT = process.env.PASEO_PORT ?? "6767";
|
||||||
|
const DAEMON_URL = `ws://127.0.0.1:${PASEO_PORT}/ws`;
|
||||||
|
|
||||||
|
async function testMultiAgentSequence() {
|
||||||
|
console.log("\n=== Testing multi-agent checkout sequence ===");
|
||||||
|
console.log(`Daemon URL: ${DAEMON_URL}`);
|
||||||
|
|
||||||
|
const client = new DaemonClientV2({
|
||||||
|
url: DAEMON_URL,
|
||||||
|
webSocketFactory: (url) => new LoggingWebSocket(url) as any,
|
||||||
|
reconnect: { enabled: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const agents: Array<{ id: string; title: string }> = [];
|
||||||
|
|
||||||
|
// Subscribe to all events for debugging
|
||||||
|
const unsub = client.subscribe((event) => {
|
||||||
|
console.log(`[Event] type=${event.type}`);
|
||||||
|
if (event.type === "session_state") {
|
||||||
|
console.log(` ${event.agents.length} agents`);
|
||||||
|
agents.length = 0;
|
||||||
|
for (const a of event.agents) {
|
||||||
|
agents.push({ id: a.id, title: a.title ?? "(untitled)" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also log ALL raw messages
|
||||||
|
client.on("session_state", (msg: any) => {
|
||||||
|
console.log(`[RAW session_state] agents=${msg.agents?.length}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also log raw messages for debugging
|
||||||
|
client.on("checkout_status_response", (msg: any) => {
|
||||||
|
console.log(`[RAW checkout_status_response] requestId=${msg.payload.requestId} agentId=${msg.payload.agentId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to connection state changes
|
||||||
|
client.subscribeConnectionStatus((state) => {
|
||||||
|
console.log(`[Connection] status=${state.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("Connected to daemon");
|
||||||
|
console.log(`Connection state: ${JSON.stringify(client.getConnectionState())}`);
|
||||||
|
|
||||||
|
// Request session state (the app does this after connecting)
|
||||||
|
console.log("Requesting session state...");
|
||||||
|
client.requestSessionState();
|
||||||
|
|
||||||
|
// Wait a bit for session state to arrive
|
||||||
|
console.log("Waiting 3s for session state...");
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
if (agents.length === 0) {
|
||||||
|
console.log("No agents found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nAvailable agents:");
|
||||||
|
for (const a of agents.slice(0, 10)) {
|
||||||
|
console.log(` - ${a.id.slice(0, 8)}... ${a.title}`);
|
||||||
|
}
|
||||||
|
if (agents.length > 10) {
|
||||||
|
console.log(` ... and ${agents.length - 10} more`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick first two agents (or use command line args)
|
||||||
|
const agent1Id = process.argv[2] ?? agents[0]?.id;
|
||||||
|
const agent2Id = process.argv[3] ?? agents[1]?.id ?? agents[0]?.id;
|
||||||
|
|
||||||
|
if (!agent1Id) {
|
||||||
|
console.log("No agents available to test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Test 1: Request checkout for agent1 (${agent1Id.slice(0, 8)}...) ===`);
|
||||||
|
const start1 = Date.now();
|
||||||
|
try {
|
||||||
|
const status1 = await client.getCheckoutStatus(agent1Id);
|
||||||
|
console.log(`✓ Agent1 completed in ${Date.now() - start1}ms - branch: ${status1.currentBranch}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`✗ Agent1 failed after ${Date.now() - start1}ms:`, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Test 2: Request checkout for agent2 (${agent2Id.slice(0, 8)}...) ===`);
|
||||||
|
const start2 = Date.now();
|
||||||
|
try {
|
||||||
|
const status2 = await client.getCheckoutStatus(agent2Id);
|
||||||
|
console.log(`✓ Agent2 completed in ${Date.now() - start2}ms - branch: ${status2.currentBranch}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`✗ Agent2 failed after ${Date.now() - start2}ms:`, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Test 3: Request checkout for agent1 again ===`);
|
||||||
|
const start3 = Date.now();
|
||||||
|
try {
|
||||||
|
const status3 = await client.getCheckoutStatus(agent1Id);
|
||||||
|
console.log(`✓ Agent1 (retry) completed in ${Date.now() - start3}ms - branch: ${status3.currentBranch}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`✗ Agent1 (retry) failed after ${Date.now() - start3}ms:`, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n=== Test 4: Request both agents in parallel ===`);
|
||||||
|
const start4 = Date.now();
|
||||||
|
try {
|
||||||
|
const [p1, p2] = await Promise.all([
|
||||||
|
client.getCheckoutStatus(agent1Id),
|
||||||
|
client.getCheckoutStatus(agent2Id),
|
||||||
|
]);
|
||||||
|
console.log(`✓ Parallel completed in ${Date.now() - start4}ms`);
|
||||||
|
console.log(` Agent1 branch: ${p1.currentBranch}`);
|
||||||
|
console.log(` Agent2 branch: ${p2.currentBranch}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`✗ Parallel failed after ${Date.now() - start4}ms:`, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Test failed:", error);
|
||||||
|
} finally {
|
||||||
|
unsub();
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Checkout Debug Script - Multi-Agent Sequence Test");
|
||||||
|
console.log("==================================================");
|
||||||
|
console.log(`PASEO_HOME: ${PASEO_HOME}`);
|
||||||
|
|
||||||
|
await testMultiAgentSequence();
|
||||||
|
|
||||||
|
console.log("\n=== Done ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
Reference in New Issue
Block a user