From 94306164bcc4cb5bfed162779d2c91ec33248a2a Mon Sep 17 00:00:00 2001 From: "Dave (AI)" Date: Sun, 12 Apr 2026 11:30:23 +1000 Subject: [PATCH] fix(cron): preserve isolated agent workspace on reload --- src/gateway/server-cron.test.ts | 73 +++++++++++++++++++++++++++++++++ src/gateway/server-cron.ts | 49 ++++++++++++++++++---- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/src/gateway/server-cron.test.ts b/src/gateway/server-cron.test.ts index 739a9c8f471..cdb4758fa1a 100644 --- a/src/gateway/server-cron.test.ts +++ b/src/gateway/server-cron.test.ts @@ -260,4 +260,77 @@ describe("buildGatewayCronService", () => { state.cron.stop(); } }); + + it("preserves explicit isolated agent workspace when runtime reload config is stale", async () => { + const tmpDir = path.join(os.tmpdir(), `server-cron-agent-workspace-${Date.now()}`); + const startupCfg = { + session: { + mainKey: "main", + }, + cron: { + store: path.join(tmpDir, "cron.json"), + }, + agents: { + defaults: { + workspace: path.join(tmpDir, "workspace"), + }, + list: [ + { id: "main", default: true }, + { id: "yinze", workspace: path.join(tmpDir, "workspace-yinze") }, + ], + }, + } as OpenClawConfig; + const reloadedCfg = { + session: { + mainKey: "main", + }, + cron: { + store: path.join(tmpDir, "cron.json"), + }, + agents: { + defaults: { + workspace: path.join(tmpDir, "workspace"), + }, + list: [{ id: "main", default: true }], + }, + } as OpenClawConfig; + loadConfigMock.mockReturnValue(reloadedCfg); + + const state = buildGatewayCronService({ + cfg: startupCfg, + deps: {} as CliDeps, + broadcast: () => {}, + }); + try { + const job = await state.cron.add({ + name: "isolated-subagent-workspace", + enabled: true, + schedule: { kind: "at", at: new Date(1).toISOString() }, + sessionTarget: "isolated", + wakeMode: "next-heartbeat", + agentId: "yinze", + payload: { kind: "agentTurn", message: "read SOW.md" }, + }); + + await state.cron.run(job.id, "force"); + + expect(runCronIsolatedAgentTurnMock).toHaveBeenCalledWith( + expect.objectContaining({ + agentId: "yinze", + cfg: expect.objectContaining({ + agents: expect.objectContaining({ + list: expect.arrayContaining([ + expect.objectContaining({ + id: "yinze", + workspace: path.join(tmpDir, "workspace-yinze"), + }), + ]), + }), + }), + }), + ); + } finally { + state.cron.stop(); + } + }); }); diff --git a/src/gateway/server-cron.ts b/src/gateway/server-cron.ts index 739cdd65dc5..b2ef95c087c 100644 --- a/src/gateway/server-cron.ts +++ b/src/gateway/server-cron.ts @@ -153,19 +153,50 @@ export function buildGatewayCronService(params: { const storePath = resolveCronStorePath(params.cfg.cron?.store); const cronEnabled = process.env.OPENCLAW_SKIP_CRON !== "1" && params.cfg.cron?.enabled !== false; + const findAgentEntry = (cfg: OpenClawConfig, agentId: string) => + Array.isArray(cfg.agents?.list) + ? cfg.agents.list.find( + (entry) => + entry && typeof entry.id === "string" && normalizeAgentId(entry.id) === agentId, + ) + : undefined; + + const hasConfiguredAgent = (cfg: OpenClawConfig, agentId: string) => + Boolean(findAgentEntry(cfg, agentId)); + + const mergeRuntimeAgentConfig = (runtimeConfig: OpenClawConfig, requestedAgentId: string) => { + if (hasConfiguredAgent(runtimeConfig, requestedAgentId)) { + return runtimeConfig; + } + const fallbackAgentEntry = findAgentEntry(params.cfg, requestedAgentId); + if (!fallbackAgentEntry) { + return runtimeConfig; + } + return { + ...runtimeConfig, + agents: { + ...params.cfg.agents, + ...runtimeConfig.agents, + defaults: { + ...params.cfg.agents?.defaults, + ...runtimeConfig.agents?.defaults, + }, + list: [...(runtimeConfig.agents?.list ?? []), fallbackAgentEntry], + }, + }; + }; + const resolveCronAgent = (requested?: string | null) => { const runtimeConfig = loadConfig(); const normalized = typeof requested === "string" && requested.trim() ? normalizeAgentId(requested) : undefined; - const hasAgent = - normalized !== undefined && - Array.isArray(runtimeConfig.agents?.list) && - runtimeConfig.agents.list.some( - (entry) => - entry && typeof entry.id === "string" && normalizeAgentId(entry.id) === normalized, - ); - const agentId = hasAgent ? normalized : resolveDefaultAgentId(runtimeConfig); - return { agentId, cfg: runtimeConfig }; + const effectiveConfig = + normalized !== undefined ? mergeRuntimeAgentConfig(runtimeConfig, normalized) : runtimeConfig; + const agentId = + normalized !== undefined && hasConfiguredAgent(effectiveConfig, normalized) + ? normalized + : resolveDefaultAgentId(effectiveConfig); + return { agentId, cfg: effectiveConfig }; }; const resolveCronSessionKey = (params: {