feat(cron): add agentId filtering to cron list

This commit is contained in:
clawsweeper
2026-05-05 00:19:29 +00:00
parent 26090a9fab
commit 1acc788f91
8 changed files with 79 additions and 3 deletions

View File

@@ -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 `<rootDir>/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 <id>] [--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 <id>` 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.

View File

@@ -211,12 +211,15 @@ Manual run and inspection:
```bash
openclaw cron list
openclaw cron list --agent ops
openclaw cron show <job-id>
openclaw cron run <job-id>
openclaw cron run <job-id> --due
openclaw cron runs --id <job-id> --limit 50
```
`openclaw cron list` shows all matching jobs by default. Pass `--agent <id>` 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:

View File

@@ -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(

View File

@@ -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(

View File

@@ -52,8 +52,9 @@ export function registerCronListCommand(cron: Command) {
const listParams: Record<string, unknown> = {
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) {

View File

@@ -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"]);
});
});

View File

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

View File

@@ -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", () => {