fix(config): avoid env-ref reload restarts

This commit is contained in:
Peter Steinberger
2026-04-25 01:22:45 +01:00
parent 8a490f4509
commit 377e254f6a
3 changed files with 92 additions and 3 deletions

View File

@@ -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.

View File

@@ -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(),

View File

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