fix(cron): preserve isolated agent workspace on reload

This commit is contained in:
Dave (AI)
2026-04-12 11:30:23 +10:00
committed by Vincent Koc
parent 3632636a86
commit 94306164bc
2 changed files with 113 additions and 9 deletions

View File

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

View File

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