fix(cron): prevent isolated cron nested lane deadlocks (#45459)

* fix(cron): resolve isolated session deadlock (#44805)

Map cron lane to nested in resolveGlobalLane to prevent deadlock when
isolated cron jobs trigger inner operations (e.g. compaction). Outer
execution holds the cron lane slot; inner work now uses nested lane.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(changelog): add cron isolated deadlock note

---------

Co-authored-by: zhujian <zhujianxyz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vincent Koc 2026-03-13 17:19:40 -04:00 committed by GitHub
parent 3e6c8376fb
commit 28b0d8e8bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 49 additions and 0 deletions

View File

@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
- Browser/existing-session: accept text-only `list_pages` and `new_page` responses from Chrome DevTools MCP so live-session tab discovery and new-tab open flows keep working when the server omits structured page metadata.
- Ollama/reasoning visibility: stop promoting native `thinking` and `reasoning` fields into final assistant text so local reasoning models no longer leak internal thoughts in normal replies. (#45330) Thanks @xi7ang.
- Cron/isolated sessions: route nested cron-triggered embedded runner work onto the nested lane so isolated cron jobs no longer deadlock when compaction or other queued inner work runs. Thanks @vincentkoc.
- Windows/gateway install: bound `schtasks` calls and fall back to the Startup-folder login item when task creation hangs, so native `openclaw gateway install` fails fast instead of wedging forever on broken Scheduled Task setups.
- Windows/gateway auth: stop attaching device identity on local loopback shared-token and password gateway calls, so native Windows agent replies no longer log stale `device signature expired` fallback noise before succeeding.
- Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus.

View File

@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { CommandLane } from "../../process/lanes.js";
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
describe("resolveGlobalLane", () => {
it("defaults to main lane when no lane is provided", () => {
expect(resolveGlobalLane()).toBe(CommandLane.Main);
expect(resolveGlobalLane("")).toBe(CommandLane.Main);
expect(resolveGlobalLane(" ")).toBe(CommandLane.Main);
});
it("maps cron lane to nested lane to prevent deadlocks", () => {
// When cron jobs trigger nested agent runs, the outer execution holds
// the cron lane slot. Inner work must use a separate lane to avoid
// deadlock. See: https://github.com/openclaw/openclaw/issues/44805
expect(resolveGlobalLane("cron")).toBe(CommandLane.Nested);
expect(resolveGlobalLane(" cron ")).toBe(CommandLane.Nested);
});
it("preserves other lanes as-is", () => {
expect(resolveGlobalLane("main")).toBe(CommandLane.Main);
expect(resolveGlobalLane("subagent")).toBe(CommandLane.Subagent);
expect(resolveGlobalLane("nested")).toBe(CommandLane.Nested);
expect(resolveGlobalLane("custom-lane")).toBe("custom-lane");
expect(resolveGlobalLane(" custom ")).toBe("custom");
});
});
describe("resolveSessionLane", () => {
it("defaults to main lane and prefixes with session:", () => {
expect(resolveSessionLane("")).toBe("session:main");
expect(resolveSessionLane(" ")).toBe("session:main");
});
it("adds session: prefix if not present", () => {
expect(resolveSessionLane("abc123")).toBe("session:abc123");
expect(resolveSessionLane(" xyz ")).toBe("session:xyz");
});
it("preserves existing session: prefix", () => {
expect(resolveSessionLane("session:abc")).toBe("session:abc");
expect(resolveSessionLane("session:main")).toBe("session:main");
});
});

View File

@ -7,6 +7,10 @@ export function resolveSessionLane(key: string) {
export function resolveGlobalLane(lane?: string) {
const cleaned = lane?.trim();
// Cron jobs hold the cron lane slot; inner operations must use nested to avoid deadlock.
if (cleaned === CommandLane.Cron) {
return CommandLane.Nested;
}
return cleaned ? cleaned : CommandLane.Main;
}