From a98660eebd2a93cc8f920d67a00ae75fda482e01 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 25 May 2026 21:10:14 +0100 Subject: [PATCH] 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. --- ...gent.direct-delivery-core-channels.test.ts | 52 ++++++++++++++++++- src/cron/isolated-agent/run.ts | 31 +++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts index c66d123acc01..69a3e3b3de20 100644 --- a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts +++ b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts @@ -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, diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 2b5e56185675..aa52bd00b810 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -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 { 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> | undefined; const loadCatalog = async () => {