diff --git a/CHANGELOG.md b/CHANGELOG.md index 460f2a9ac84..c4b448d5e5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +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. +- Cron CLI: add `openclaw cron list --agent `, normalize the requested agent id, and include jobs without a stored agent id under the configured default agent 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 095b41f7092..f836b6520c1 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -218,7 +218,7 @@ 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. +`openclaw cron list` shows all matching jobs by default. Pass `--agent ` to show only jobs whose effective normalized agent id matches; jobs without a stored agent id count as the configured default agent. `cron runs` entries include delivery diagnostics with the intended cron target, the resolved target, message-tool sends, fallback use, and delivered state. diff --git a/src/cron/service.list-page-sort-guards.test.ts b/src/cron/service.list-page-sort-guards.test.ts index c7c432a0456..26a85fa0f75 100644 --- a/src/cron/service.list-page-sort-guards.test.ts +++ b/src/cron/service.list-page-sort-guards.test.ts @@ -64,6 +64,33 @@ describe("cron listPage sort guards", () => { expect(page.jobs.map((job) => job.id)).toEqual(["job-ops"]); }); + it("matches omitted job agent ids to the configured default agent when 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 }); + state.deps.defaultAgentId = " Ops "; + + const page = await listPage(state, { agentId: "ops" }); + + expect(page.jobs.map((job) => job.id)).toEqual(["job-ops", "job-unset"]); + }); + + it("matches omitted job agent ids to main when no default agent is configured", 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: "main" }); + + expect(page.jobs.map((job) => job.id)).toEqual(["job-main", "job-unset"]); + }); + it("keeps listPage unfiltered when agent id is omitted", async () => { const jobs = [ createBaseJob({ id: "job-main", agentId: "main", name: "main" }), diff --git a/src/cron/service/ops.ts b/src/cron/service/ops.ts index bf088c40bfc..db35a540d47 100644 --- a/src/cron/service/ops.ts +++ b/src/cron/service/ops.ts @@ -1,5 +1,6 @@ import { enqueueCommandInLane } from "../../process/command-queue.js"; import { CommandLane } from "../../process/lanes.js"; +import { DEFAULT_AGENT_ID } from "../../routing/session-key.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { completeTaskRunByRunId, @@ -273,6 +274,14 @@ function sortJobs(jobs: CronJob[], sortBy: CronJobsSortBy, sortDir: CronSortDir) }); } +function resolveEffectiveJobAgentId(job: CronJob, defaultAgentId: string | undefined) { + return ( + normalizeOptionalAgentId(job.agentId) ?? + normalizeOptionalAgentId(defaultAgentId) ?? + DEFAULT_AGENT_ID + ); +} + export async function listPage(state: CronServiceState, opts?: CronListPageOptions) { return await locked(state, async () => { await ensureLoadedForRead(state); @@ -289,7 +298,10 @@ export async function listPage(state: CronServiceState, opts?: CronListPageOptio if (enabledFilter === "disabled" && isJobEnabled(job)) { return false; } - if (requestedAgentId && job.agentId !== requestedAgentId) { + if ( + requestedAgentId && + resolveEffectiveJobAgentId(job, state.deps.defaultAgentId) !== requestedAgentId + ) { return false; } if (!query) {