diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e8b8761f4..460f2a9ac84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai - Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys. - Secrets/external channel contracts: also look in `/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss. - Models/auth: add `openclaw models auth list [--provider ] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc. +- Cron CLI: add `openclaw cron list --agent ` and normalize the requested agent id before filtering, while keeping `cron list` unfiltered when no agent is supplied. Fixes #77118. Thanks @zhanggttry. - Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar. - Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev. - Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure. diff --git a/docs/cli/cron.md b/docs/cli/cron.md index 754d61f151e..095b41f7092 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -211,12 +211,15 @@ Manual run and inspection: ```bash openclaw cron list +openclaw cron list --agent ops openclaw cron show openclaw cron run openclaw cron run --due openclaw cron runs --id --limit 50 ``` +`openclaw cron list` shows all matching jobs by default. Pass `--agent ` to show only jobs pinned to that normalized agent id. + `cron runs` entries include delivery diagnostics with the intended cron target, the resolved target, message-tool sends, fallback use, and delivered state. Agent and session retargeting: diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 8c228ce03be..28efe59a0f7 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -210,6 +210,34 @@ describe("cron tool", () => { expect(callGatewayMock).not.toHaveBeenCalled(); }); + it("filters cron list by the requester agent session", async () => { + const tool = createTestCronTool({ + agentSessionKey: "agent:agent-123:telegram:direct:channing", + }); + + await tool.execute("call-list", { + action: "list", + }); + + const params = expectSingleGatewayCallMethod("cron.list"); + expect(params).toEqual({ includeDisabled: false, agentId: "agent-123" }); + }); + + it("prefers explicit cron list agent id over the requester session", async () => { + const tool = createTestCronTool({ + agentSessionKey: "agent:agent-123:telegram:direct:channing", + }); + + await tool.execute("call-list-explicit", { + action: "list", + agentId: "ops", + includeDisabled: true, + }); + + const params = expectSingleGatewayCallMethod("cron.list"); + expect(params).toEqual({ includeDisabled: true, agentId: "ops" }); + }); + it("documents deferred follow-up guidance in the tool description", () => { const tool = createTestCronTool(); expect(tool.description).toContain( diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index f50fb26fe7b..0b8d7d70445 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -481,6 +481,20 @@ describe("cron cli", () => { expect(patch.enabled).toBe(expectedEnabled); }); + it("leaves cron list unfiltered when --agent is omitted", async () => { + await runCronCommand(["cron", "list"]); + + const listCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.list"); + expect(listCall?.[2]).toEqual({ includeDisabled: false }); + }); + + it("sends normalized agent id on cron list --agent", async () => { + await runCronCommand(["cron", "list", "--agent", " Ops "]); + + const listCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.list"); + expect(listCall?.[2]).toEqual({ includeDisabled: false, agentId: "ops" }); + }); + it("paginates cron show lookups", async () => { resetGatewayMock(); callGatewayFromCli.mockImplementation( diff --git a/src/cli/cron-cli/register.cron-add.ts b/src/cli/cron-cli/register.cron-add.ts index be1cd74645f..232677b1992 100644 --- a/src/cli/cron-cli/register.cron-add.ts +++ b/src/cli/cron-cli/register.cron-add.ts @@ -52,8 +52,9 @@ export function registerCronListCommand(cron: Command) { const listParams: Record = { includeDisabled: Boolean(opts.all), }; - if (opts.agent) { - listParams.agentId = opts.agent; + const agentId = normalizeOptionalString(opts.agent); + if (agentId) { + listParams.agentId = sanitizeAgentId(agentId); } const res = await callGatewayFromCli("cron.list", opts, listParams); if (opts.json) { diff --git a/src/cron/service.list-page-sort-guards.test.ts b/src/cron/service.list-page-sort-guards.test.ts index 69349147adf..c7c432a0456 100644 --- a/src/cron/service.list-page-sort-guards.test.ts +++ b/src/cron/service.list-page-sort-guards.test.ts @@ -50,4 +50,29 @@ describe("cron listPage sort guards", () => { const page = await listPage(state, { sortBy: "nextRunAtMs", sortDir: "asc" }); expect(page.jobs).toHaveLength(2); }); + + it("normalizes requested agent ids before filtering", async () => { + const jobs = [ + createBaseJob({ id: "job-main", agentId: "main", name: "main" }), + createBaseJob({ id: "job-ops", agentId: "ops", name: "ops" }), + createBaseJob({ id: "job-unset", agentId: undefined, name: "unset" }), + ]; + const state = createMockCronStateForJobs({ jobs }); + + const page = await listPage(state, { agentId: " Ops " }); + + expect(page.jobs.map((job) => job.id)).toEqual(["job-ops"]); + }); + + it("keeps listPage unfiltered when agent id is omitted", async () => { + const jobs = [ + createBaseJob({ id: "job-main", agentId: "main", name: "main" }), + createBaseJob({ id: "job-ops", agentId: "ops", name: "ops" }), + ]; + const state = createMockCronStateForJobs({ jobs }); + + const page = await listPage(state); + + expect(page.jobs.map((job) => job.id)).toEqual(["job-main", "job-ops"]); + }); }); diff --git a/src/cron/service/ops.ts b/src/cron/service/ops.ts index e2645b9d802..bf088c40bfc 100644 --- a/src/cron/service/ops.ts +++ b/src/cron/service/ops.ts @@ -30,6 +30,7 @@ import type { CronSortDir, } from "./list-page-types.js"; import { locked } from "./locked.js"; +import { normalizeOptionalAgentId } from "./normalize.js"; import type { CronServiceState } from "./state.js"; import { ensureLoaded, persist, warnIfDisabled } from "./store.js"; import { @@ -279,6 +280,7 @@ export async function listPage(state: CronServiceState, opts?: CronListPageOptio const enabledFilter = resolveEnabledFilter(opts); const sortBy = opts?.sortBy ?? "nextRunAtMs"; const sortDir = opts?.sortDir ?? "asc"; + const requestedAgentId = normalizeOptionalAgentId(opts?.agentId); const source = state.store?.jobs ?? []; const filtered = source.filter((job) => { if (enabledFilter === "enabled" && !isJobEnabled(job)) { @@ -287,7 +289,7 @@ export async function listPage(state: CronServiceState, opts?: CronListPageOptio if (enabledFilter === "disabled" && isJobEnabled(job)) { return false; } - if (opts?.agentId && job.agentId !== opts.agentId) { + if (requestedAgentId && job.agentId !== requestedAgentId) { return false; } if (!query) { diff --git a/src/gateway/protocol/cron-validators.test.ts b/src/gateway/protocol/cron-validators.test.ts index 3d869131580..69073b93ef4 100644 --- a/src/gateway/protocol/cron-validators.test.ts +++ b/src/gateway/protocol/cron-validators.test.ts @@ -111,9 +111,11 @@ describe("cron protocol validators", () => { enabled: "all", sortBy: "nextRunAtMs", sortDir: "asc", + agentId: "ops", }), ).toBe(true); expect(validateCronListParams({ offset: -1 })).toBe(false); + expect(validateCronListParams({ agentId: "" })).toBe(false); }); it("enforces runs limit minimum for id and jobId selectors", () => {