Skip to content

Commit e844d1d

Browse files
committed
fix(cron): restore suspended lanes to default concurrency
1 parent a61d530 commit e844d1d

4 files changed

Lines changed: 49 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
3535
- Plugins/perf: reuse derived plugin metadata snapshots for the lifetime of the process so reply-time skill setup no longer rescans plugin metadata on every turn.
3636
- Discord/OpenAI voice: keep wake-name master consults using the current speaker context after ignored ambient transcripts and shorten the default capture silence grace.
3737
- Doctor: skip redundant Gateway restart prompts when a recent supervisor restart leaves the Gateway healthy. Fixes #86518. (#86533) Thanks @liaoyl830.
38+
- Cron: restore suspended cron lanes to the configured/default concurrency instead of falling back to one after quota or circuit-breaker auto-resume.
3839
- Gateway: keep session-only Control UI tool-start mirrors flowing during diagnostic queue pressure instead of silently dropping non-terminal tool updates.
3940
- Agents/memory: return optional not-found context for missing date-only daily memory reads instead of logging benign first-run `ENOENT` failures. Fixes #82928. Thanks @galiniliev.
4041
- Discord: merge streamed text captions into following media block replies so captions and attachments send as one message. (#86487) Thanks @neeravmakwana.

src/agents/session-suspension.test.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { DEFAULT_CRON_MAX_CONCURRENT_RUNS } from "../config/cron-limits.js";
23
import type { OpenClawConfig } from "../config/types.openclaw.js";
34
import { CommandLane } from "../process/lanes.js";
45

@@ -23,12 +24,12 @@ vi.mock("./command/session.js", () => ({
2324
}),
2425
}));
2526

26-
async function suspendMainLane(ttlMs: number, cfg: OpenClawConfig) {
27+
async function suspendLane(ttlMs: number, cfg: OpenClawConfig, laneId: CommandLane) {
2728
const { suspendSession } = await import("./session-suspension.js");
2829
await suspendSession({
2930
cfg,
3031
sessionId: "session-1",
31-
laneId: CommandLane.Main,
32+
laneId,
3233
reason: "quota_exhausted",
3334
failedProvider: "anthropic",
3435
failedModel: "claude-opus-4-6",
@@ -40,6 +41,8 @@ describe("session suspension", () => {
4041
afterEach(async () => {
4142
const { cancelLaneAutoResume } = await import("./session-suspension.js");
4243
cancelLaneAutoResume(CommandLane.Main);
44+
cancelLaneAutoResume(CommandLane.Cron);
45+
cancelLaneAutoResume(CommandLane.CronNested);
4346
vi.useRealTimers();
4447
sessionStoreMocks.updateSessionStoreEntry.mockClear();
4548
commandQueueMocks.setCommandLaneConcurrency.mockClear();
@@ -51,7 +54,7 @@ describe("session suspension", () => {
5154
agents: { defaults: { maxConcurrent: 4 } },
5255
} as OpenClawConfig;
5356

54-
await suspendMainLane(100, cfg);
57+
await suspendLane(100, cfg, CommandLane.Main);
5558

5659
expect(commandQueueMocks.setCommandLaneConcurrency).toHaveBeenCalledWith(CommandLane.Main, 0);
5760

@@ -63,6 +66,44 @@ describe("session suspension", () => {
6366
);
6467
});
6568

69+
it("auto-resumes cron lanes to the cron concurrency default", async () => {
70+
vi.useFakeTimers();
71+
72+
await suspendLane(100, {} as OpenClawConfig, CommandLane.CronNested);
73+
74+
expect(commandQueueMocks.setCommandLaneConcurrency).toHaveBeenCalledWith(
75+
CommandLane.CronNested,
76+
0,
77+
);
78+
79+
await vi.advanceTimersByTimeAsync(100);
80+
81+
expect(commandQueueMocks.setCommandLaneConcurrency).toHaveBeenLastCalledWith(
82+
CommandLane.CronNested,
83+
DEFAULT_CRON_MAX_CONCURRENT_RUNS,
84+
);
85+
});
86+
87+
it("auto-resumes cron lanes to configured and clamped cron concurrency", async () => {
88+
vi.useFakeTimers();
89+
90+
await suspendLane(100, { cron: { maxConcurrentRuns: 3 } } as OpenClawConfig, CommandLane.Cron);
91+
await vi.advanceTimersByTimeAsync(100);
92+
93+
expect(commandQueueMocks.setCommandLaneConcurrency).toHaveBeenLastCalledWith(
94+
CommandLane.Cron,
95+
3,
96+
);
97+
98+
await suspendLane(100, { cron: { maxConcurrentRuns: 0 } } as OpenClawConfig, CommandLane.Cron);
99+
await vi.advanceTimersByTimeAsync(100);
100+
101+
expect(commandQueueMocks.setCommandLaneConcurrency).toHaveBeenLastCalledWith(
102+
CommandLane.Cron,
103+
1,
104+
);
105+
});
106+
66107
it("maps failover reasons to persisted suspension reasons", async () => {
67108
const { testing } = await import("./session-suspension.js");
68109

src/agents/session-suspension.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from "node:path";
22
import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js";
3+
import { resolveCronMaxConcurrentRuns } from "../config/cron-limits.js";
34
import { updateSessionStoreEntry } from "../config/sessions.js";
45
import type { OpenClawConfig } from "../config/types.openclaw.js";
56
import { createSubsystemLogger } from "../logging/subsystem.js";
@@ -23,10 +24,8 @@ function resolveLaneResumeConcurrency(cfg: OpenClawConfig | undefined, laneId: s
2324
case "subagent":
2425
return resolveSubagentMaxConcurrent(cfg);
2526
case "cron":
26-
case "cron-nested": {
27-
const raw = cfg?.cron?.maxConcurrentRuns;
28-
return typeof raw === "number" && Number.isFinite(raw) ? Math.max(1, Math.floor(raw)) : 1;
29-
}
27+
case "cron-nested":
28+
return resolveCronMaxConcurrentRuns(cfg?.cron);
3029
default:
3130
return DEFAULT_CUSTOM_LANE_RESUME_CONCURRENCY;
3231
}

src/cron/service/timer.regression.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,7 @@ describe("cron service timer regressions", () => {
10021002
const state = createCronServiceState({
10031003
cronEnabled: true,
10041004
storePath: store.storePath,
1005+
cronConfig: { maxConcurrentRuns: 1 },
10051006
log: noopLogger,
10061007
nowMs: () => now,
10071008
enqueueSystemEvent: vi.fn(),
@@ -1013,7 +1014,6 @@ describe("cron service timer regressions", () => {
10131014
now += params.job.id === first.id ? 50 : 20;
10141015
return { status: "ok" as const, summary: "ok" };
10151016
}),
1016-
cronConfig: { maxConcurrentRuns: 1 },
10171017
});
10181018

10191019
await onTimer(state);

0 commit comments

Comments
 (0)