From a4b15455b7c2ff82bee656f93c53d4895a57118c Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:55:27 +0200 Subject: [PATCH] fix(cli): list every agent in paseo agent ls, past the first 200 (#6033) * fix(cli): list every agent page in agent ls agent ls made one fetch_agents call and ignored pageInfo.nextCursor, so it stopped at the daemon's default page of 200 agents without saying more existed. Follow the cursor until the daemon reports no next page, as workspace ls and agent delete already do. Co-authored-by: HTeo * fix(cli): page agent ls in creation order fetch_agents defaults to updated_at order, so an agent that updated after the first page moved ahead of the cursor and was skipped. Page by created_at, which never changes; ls sorts the rows itself afterwards. * test(cli): exercise agent listing with a supplied client --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: HTeo --- packages/cli/src/commands/agent/ls.test.ts | 67 +++++++++++++++++++++- packages/cli/src/commands/agent/ls.ts | 34 ++++++++++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/agent/ls.test.ts b/packages/cli/src/commands/agent/ls.test.ts index 8aedf73fad..76b1787ce3 100644 --- a/packages/cli/src/commands/agent/ls.test.ts +++ b/packages/cli/src/commands/agent/ls.test.ts @@ -1,5 +1,60 @@ import { describe, expect, it } from "vitest"; -import { buildAgentLsFetchOptions } from "./ls.js"; +import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; +import { buildAgentLsFetchOptions, listAgents } from "./ls.js"; + +const daemonTarget = { kind: "endpoint" as const, host: "example.test:12345" }; + +function createPagingClient() { + const daemonAgents: AgentSnapshotPayload[] = [3, 2, 1].map((n) => ({ + id: `${n}${n}${n}${n}${n}${n}${n}${n}-0000-4000-8000-000000000000`, + provider: "codex", + title: `agent ${n}`, + status: "closed", + archivedAt: null, + cwd: "/tmp/project", + createdAt: `2026-10-0${n}T00:00:00.000Z`, + updatedAt: `2026-10-0${n}T00:00:00.000Z`, + labels: {}, + model: null, + lastUserMessageAt: null, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: false, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + })); + + // The fake daemon pages two agents at a time, newest first by the requested key + // (updated_at by default), and agent 1 is updated right after the first page is served. + const fetchAgents = async (options?: { + sort?: { key: string }[]; + page?: { cursor?: string }; + }) => { + const key: "createdAt" | "updatedAt" = + options?.sort?.[0]?.key === "created_at" ? "createdAt" : "updatedAt"; + const cursor = options?.page?.cursor; + const ordered = daemonAgents + .filter((agent) => cursor === undefined || agent[key] < cursor) + .sort((left, right) => right[key].localeCompare(left[key])); + const page = ordered.slice(0, 2); + const hasMore = ordered.length > 2; + if (cursor === undefined) { + daemonAgents[2]!.updatedAt = "2026-10-09T00:00:00.000Z"; + } + return { + entries: page.map((agent) => ({ agent })), + pageInfo: { nextCursor: hasMore ? page[page.length - 1]![key] : null }, + }; + }; + return { fetchAgents, close: async () => {} }; +} describe("buildAgentLsFetchOptions", () => { it("fetches active agents by default", () => { @@ -66,3 +121,13 @@ describe("buildAgentLsFetchOptions", () => { }); }); }); + +describe("runLsCommand", () => { + it("lists agents on every page, including one updated during the listing", async () => { + const result = await listAgents({ daemonTarget, all: true, global: true }, async () => + createPagingClient(), + ); + + expect(result.data.map((item) => item.name)).toEqual(["agent 3", "agent 2", "agent 1"]); + }); +}); diff --git a/packages/cli/src/commands/agent/ls.ts b/packages/cli/src/commands/agent/ls.ts index 0990446bc5..55674a2f87 100644 --- a/packages/cli/src/commands/agent/ls.ts +++ b/packages/cli/src/commands/agent/ls.ts @@ -165,6 +165,14 @@ export function buildAgentLsFetchOptions( return fetchOptions; } +interface AgentLsClient { + fetchAgents(options?: FetchAgentsOptions): Promise<{ + entries: { agent: AgentSnapshotPayload }[]; + pageInfo: { nextCursor: string | null }; + }>; + close(): Promise; +} + /** * Agent ls command semantics: * - `paseo agent ls` → active non-archived agents @@ -176,7 +184,16 @@ export async function runLsCommand( options: AgentLsOptions, _command: Command, ): Promise { - const client = await connectToDaemon({ target: options.daemonTarget }); + return listAgents(options); +} + +export async function listAgents( + options: AgentLsOptions, + connect: ( + options: Parameters[0], + ) => Promise = connectToDaemon, +): Promise { + const client = await connect({ target: options.daemonTarget }); try { const normalizedThinkingOptionId = options.thinking?.trim(); @@ -189,8 +206,19 @@ export async function runLsCommand( } const labelFilters = parseLabelFilters(options.label); - const fetchPayload = await client.fetchAgents(buildAgentLsFetchOptions(options)); - let agents = fetchPayload.entries.map((entry) => entry.agent); + const fetchOptions = buildAgentLsFetchOptions(options); + let agents: AgentSnapshotPayload[] = []; + let cursor: string | undefined; + // createdAt never changes, so agents cannot move past the cursor while the pages are read. + do { + const payload = await client.fetchAgents({ + ...fetchOptions, + sort: [{ key: "created_at", direction: "desc" }], + page: { limit: 200, ...(cursor ? { cursor } : {}) }, + }); + agents.push(...payload.entries.map((entry) => entry.agent)); + cursor = payload.pageInfo.nextCursor ?? undefined; + } while (cursor); // By default, exclude archived agents. `-a` includes them. if (!options.all) {