fix(cron): preserve runtime snapshot for isolated delivery

Fix isolated cron delivery so agent-default derivation keeps using the paired runtime config snapshot, preserving resolved channel credentials such as Discord SecretRefs. Fixes #86545.
This commit is contained in:
Peter Steinberger
2026-05-25 21:10:14 +01:00
committed by GitHub
parent c55bee5ec7
commit a98660eebd
2 changed files with 77 additions and 6 deletions

View File

@@ -1,8 +1,9 @@
import "./isolated-agent.mocks.js";
import { beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
import type { ChannelOutboundAdapter, ChannelOutboundContext } from "../channels/plugins/types.js";
import type { CliDeps } from "../cli/deps.js";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import { resolveOutboundSendDep } from "../infra/outbound/send-deps.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
@@ -297,6 +298,10 @@ describe("runCronIsolatedAgentTurn core-channel direct delivery", () => {
);
});
afterEach(() => {
clearRuntimeConfigSnapshot();
});
for (const testCase of CASES) {
it(`routes ${testCase.name} text-only announce delivery through the outbound adapter`, async () => {
await expectCoreChannelAnnounceDelivery({
@@ -316,6 +321,51 @@ describe("runCronIsolatedAgentTurn core-channel direct delivery", () => {
});
if (testCase.channel === "discord") {
it("keeps isolated Discord delivery on the active runtime snapshot after agent-default derivation", async () => {
await withTempCronHome(async (home) => {
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
const sourceCfg = makeCfg(home, storePath, {
channels: {
discord: {
accounts: {
default: {
token: { provider: "default", source: "env", id: "DISCORD_BOT_TOKEN" },
},
},
},
},
});
const runtimeCfg = makeCfg(home, storePath, {
channels: {
discord: {
accounts: { default: { token: "resolved-discord-token" } },
},
},
});
setRuntimeConfigSnapshot(runtimeCfg, sourceCfg);
const deps = createCliDeps();
mockAgentPayloads([{ text: "hello from cron" }]);
const res = await runExplicitAnnounceTurn({
cfg: sourceCfg,
deps,
channel: "discord",
to: testCase.to,
});
expect(res.status).toBe("ok");
expect(res.delivered).toBe(true);
expect(deps.sendMessageDiscord).toHaveBeenCalledTimes(1);
expect(deps.sendMessageDiscord).toHaveBeenCalledWith(
testCase.expectedTo,
"hello from cron",
expect.objectContaining({
cfg: expect.objectContaining({ channels: runtimeCfg.channels }),
}),
);
});
});
it("collapses Discord text-only announce delivery to the final assistant text", async () => {
await expectCoreChannelAnnounceDelivery({
testCase,

View File

@@ -5,6 +5,11 @@ import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js";
import type { SkillSnapshot } from "../../agents/skills.js";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import type { CliDeps } from "../../cli/outbound-send-deps.js";
import {
getRuntimeConfigSnapshot,
getRuntimeConfigSourceSnapshot,
selectApplicableRuntimeConfig,
} from "../../config/config.js";
import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { clearAgentRunContext } from "../../infra/agent-events.js";
@@ -480,12 +485,28 @@ type CronPreparationResult =
| { ok: true; context: PreparedCronRunContext }
| { ok: false; result: RunCronAgentTurnResult };
function resolveCronActiveRuntimeConfig(cfg: OpenClawConfig): OpenClawConfig {
const runtimeConfig = getRuntimeConfigSnapshot();
const runtimeSourceConfig = getRuntimeConfigSourceSnapshot();
if (!runtimeConfig || !runtimeSourceConfig) {
return cfg;
}
return (
selectApplicableRuntimeConfig({
inputConfig: cfg,
runtimeConfig,
runtimeSourceConfig,
}) ?? cfg
);
}
async function prepareCronRunContext(params: {
input: RunCronAgentTurnParams;
isFastTestEnv: boolean;
}): Promise<CronPreparationResult> {
const { input } = params;
const defaultAgentId = resolveDefaultAgentId(input.cfg);
const runtimeCfg = resolveCronActiveRuntimeConfig(input.cfg);
const defaultAgentId = resolveDefaultAgentId(runtimeCfg);
const requestedAgentId =
typeof input.agentId === "string" && input.agentId.trim()
? input.agentId
@@ -494,16 +515,16 @@ async function prepareCronRunContext(params: {
: undefined;
const normalizedRequested = requestedAgentId ? normalizeAgentId(requestedAgentId) : undefined;
const agentConfigOverride = normalizedRequested
? resolveAgentConfig(input.cfg, normalizedRequested)
? resolveAgentConfig(runtimeCfg, normalizedRequested)
: undefined;
const agentId = normalizedRequested ?? defaultAgentId;
const agentCfg: AgentDefaultsConfig = buildCronAgentDefaultsConfig({
defaults: input.cfg.agents?.defaults,
defaults: runtimeCfg.agents?.defaults,
agentConfigOverride,
});
const cfgWithAgentDefaults: OpenClawConfig = {
...input.cfg,
agents: Object.assign({}, input.cfg.agents, { defaults: agentCfg }),
...runtimeCfg,
agents: Object.assign({}, runtimeCfg.agents, { defaults: agentCfg }),
};
let catalog: Awaited<ReturnType<CronModelCatalogRuntime["loadModelCatalog"]>> | undefined;
const loadCatalog = async () => {