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 <hteo1337@gmail.com>

* 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 <hteo1337@gmail.com>
This commit is contained in:
paseo-bot[bot]
2026-10-14 03:55:27 +02:00
committed by GitHub
co-authored by HTeo
parent bd9af256bd
commit a4b15455b7
2 changed files with 97 additions and 4 deletions
+66 -1
View File
@@ -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"]);
});
});
+31 -3
View File
@@ -165,6 +165,14 @@ export function buildAgentLsFetchOptions(
return fetchOptions;
}
interface AgentLsClient {
fetchAgents(options?: FetchAgentsOptions): Promise<{
entries: { agent: AgentSnapshotPayload }[];
pageInfo: { nextCursor: string | null };
}>;
close(): Promise<void>;
}
/**
* Agent ls command semantics:
* - `paseo agent ls` → active non-archived agents
@@ -176,7 +184,16 @@ export async function runLsCommand(
options: AgentLsOptions,
_command: Command,
): Promise<AgentLsResult> {
const client = await connectToDaemon({ target: options.daemonTarget });
return listAgents(options);
}
export async function listAgents(
options: AgentLsOptions,
connect: (
options: Parameters<typeof connectToDaemon>[0],
) => Promise<AgentLsClient> = connectToDaemon,
): Promise<AgentLsResult> {
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) {