diff --git a/CHANGELOG.md b/CHANGELOG.md index 3428dd41a10..b4a077b46af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Browser/tool: keep explicit AI snapshots from inheriting the efficient role-snapshot default and preserve numeric Playwright AI refs, so `--format ai` remains a real AI snapshot path. Fixes #62550. Thanks @ly85206559. +- Gateway/config: keep in-process config patch reload comparisons on the resolved source snapshot when `${VAR}` env refs are restored on disk, avoiding false full gateway restarts for unchanged gateway/plugin secrets. Fixes #71208. Thanks @robbiethompson18. - Slack/messages: serialize write-client requests and whole outbound sends per target so rapid multi-message Slack replies preserve send order. Fixes #69101. (#69105) Thanks @nightq and @ztexydt-cqh. - Slack/messages: keep Slack bot tokens out of internal message-ordering and DM cache keys. - Slack/exec approvals: resolve native approval button clicks over the Gateway instead of delivering `/approve ...` as plain agent text, preserving retry buttons if Gateway resolution fails. Fixes #71023. (#71025) Thanks @marusan03. diff --git a/src/config/io.ts b/src/config/io.ts index 9409faf1f1b..5d17a4a2bab 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -2050,7 +2050,6 @@ export async function writeConfigFile( ) { return; } - const committedSourceConfig = writeResult.persistedConfig ?? nextCfg; const notifyCommittedWrite = () => { const currentRuntimeConfig = getRuntimeConfigSnapshotState(); if (!currentRuntimeConfig) { @@ -2058,7 +2057,7 @@ export async function writeConfigFile( } notifyRuntimeConfigWriteListeners({ configPath: io.configPath, - sourceConfig: committedSourceConfig, + sourceConfig: nextCfg, runtimeConfig: currentRuntimeConfig, persistedHash: writeResult.persistedHash, writtenAtMs: Date.now(), @@ -2067,7 +2066,7 @@ export async function writeConfigFile( // Keep the last-known-good runtime snapshot active until the specialized refresh path // succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh. await finalizeRuntimeSnapshotWrite({ - nextSourceConfig: committedSourceConfig, + nextSourceConfig: nextCfg, hadRuntimeSnapshot, hadBothSnapshots, loadFreshConfig: () => io.loadConfig(), diff --git a/src/config/io.write-config.test.ts b/src/config/io.write-config.test.ts index c7dd0f3ed22..3e3c03f7040 100644 --- a/src/config/io.write-config.test.ts +++ b/src/config/io.write-config.test.ts @@ -5,6 +5,7 @@ import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js"; import { createConfigIO, + registerConfigWriteListener, resetConfigRuntimeState, setRuntimeConfigSnapshot, writeConfigFile, @@ -532,4 +533,92 @@ describe("config io write", () => { } }); }); + + it("notifies in-process reloaders with resolved source config when persisted env refs are restored", async () => { + await withSuiteHome(async (home) => { + const configPath = path.join(home, ".openclaw", "openclaw.json"); + const previousConfigPath = process.env.OPENCLAW_CONFIG_PATH; + const previousGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_CONFIG_PATH = configPath; + process.env.OPENCLAW_GATEWAY_TOKEN = "gateway-token-runtime"; + await fs.mkdir(path.dirname(configPath), { recursive: true }); + await fs.writeFile( + configPath, + `${JSON.stringify( + { + gateway: { + mode: "local", + auth: { mode: "token", token: "${OPENCLAW_GATEWAY_TOKEN}" }, + }, + agents: { defaults: { model: { primary: "openai/gpt-5.4" } } }, + }, + null, + 2, + )}\n`, + "utf-8", + ); + const observedSources: unknown[] = []; + const unsubscribe = registerConfigWriteListener((event) => { + observedSources.push(event.sourceConfig); + }); + + try { + setRuntimeConfigSnapshot( + { + gateway: { + mode: "local", + auth: { mode: "token", token: "gateway-token-runtime" }, + }, + agents: { defaults: { model: { primary: "openai/gpt-5.4" } } }, + }, + { + gateway: { + mode: "local", + auth: { mode: "token", token: "gateway-token-runtime" }, + }, + agents: { defaults: { model: { primary: "openai/gpt-5.4" } } }, + }, + ); + + await writeConfigFile({ + gateway: { + mode: "local", + auth: { mode: "token", token: "gateway-token-runtime" }, + }, + agents: { defaults: { model: { primary: "openrouter/anthropic/claude-sonnet-4.6" } } }, + }); + + expect(JSON.parse(await fs.readFile(configPath, "utf-8"))).toMatchObject({ + gateway: { + auth: { token: "${OPENCLAW_GATEWAY_TOKEN}" }, + }, + }); + expect(observedSources).toEqual([ + expect.objectContaining({ + gateway: { + mode: "local", + auth: { mode: "token", token: "gateway-token-runtime" }, + }, + agents: { + defaults: { + model: { primary: "openrouter/anthropic/claude-sonnet-4.6" }, + }, + }, + }), + ]); + } finally { + unsubscribe(); + if (previousConfigPath === undefined) { + delete process.env.OPENCLAW_CONFIG_PATH; + } else { + process.env.OPENCLAW_CONFIG_PATH = previousConfigPath; + } + if (previousGatewayToken === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previousGatewayToken; + } + } + }); + }); });