Update files

This commit is contained in:
Mohamed Boudra
2026-01-26 14:09:07 +07:00
parent be235e14d7
commit fe5ab731e0
3 changed files with 252 additions and 15 deletions

View File

@@ -192,6 +192,10 @@ export interface ProjectGroup {
projectKey: string;
projectName: string;
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 {
@@ -204,7 +208,7 @@ export interface GroupedAgents {
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 {
/**
@@ -214,9 +218,15 @@ interface GroupAgentsOptions {
getRemoteUrl?: (agent: AggregatedAgent) => string | null;
}
const MAX_INACTIVE_PER_PROJECT = 5;
/**
* 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(
agents: AggregatedAgent[],
@@ -240,28 +250,61 @@ export function groupAgents(
}
}
// Group active agents by project
const projectMap = new Map<string, AggregatedAgent[]>();
// Group active agents by project, tracking truly active vs recently active
const projectMap = new Map<
string,
{ trulyActive: AggregatedAgent[]; recentlyActive: AggregatedAgent[] }
>();
for (const agent of activeAgents) {
const remoteKey = deriveRemoteProjectKey(
options?.getRemoteUrl?.(agent) ?? null
);
const projectKey = remoteKey ?? deriveProjectKey(agent.cwd);
const existing = projectMap.get(projectKey) || [];
existing.push(agent);
const existing = projectMap.get(projectKey) || {
trulyActive: [],
recentlyActive: [],
};
const isTrulyActive =
agent.status === "running" || agent.requiresAttention;
if (isTrulyActive) {
existing.trulyActive.push(agent);
} else {
existing.recentlyActive.push(agent);
}
projectMap.set(projectKey, existing);
}
// Sort agents within each project by lastActivityAt (newest first)
// Build project groups with limits applied
const activeGroups: ProjectGroup[] = [];
for (const [projectKey, projectAgents] of projectMap) {
projectAgents.sort(
for (const [projectKey, { trulyActive, recentlyActive }] of projectMap) {
// Sort both arrays by lastActivityAt (newest first)
trulyActive.sort(
(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({
projectKey,
projectName: deriveProjectName(projectKey),
agents: projectAgents,
agents: combinedAgents,
activeCount: trulyActive.length,
totalCount: trulyActive.length + recentlyActive.length,
});
}

View File

@@ -1046,6 +1046,10 @@ export class DaemonClientV2 {
if (!requestId) {
const existing = this.checkoutStatusInFlight.get(agentId);
if (existing) {
this.logger.debug(
{ agentId, inFlightCount: this.checkoutStatusInFlight.size },
"getCheckoutStatus: returning existing in-flight request"
);
return existing;
}
}
@@ -1057,6 +1061,11 @@ export class DaemonClientV2 {
requestId: resolvedRequestId,
});
this.logger.debug(
{ agentId, requestId: resolvedRequestId, waiterCount: this.waiters.size },
"getCheckoutStatus: creating new request"
);
const responsePromise = (async () => {
const response = this.waitFor(
(msg) => {
@@ -1077,11 +1086,24 @@ export class DaemonClientV2 {
if (!requestId) {
this.checkoutStatusInFlight.set(agentId, responsePromise);
responsePromise.finally(() => {
if (this.checkoutStatusInFlight.get(agentId) === responsePromise) {
this.checkoutStatusInFlight.delete(agentId);
}
});
responsePromise
.then(() => {
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;
@@ -2099,6 +2121,10 @@ export class DaemonClientV2 {
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({
status: "disconnected",
...(reason ? { reason } : {}),

View File

@@ -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);