mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(config): align in-process write sourceConfig with file-watcher (#73267)
Fix config writes so in-process reload notifications use the canonical post-write source snapshot, matching the file watcher path. Adds regression coverage for the runtime source snapshot and changelog credit.
This commit is contained in:
@@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Channels/Telegram: keep Bot API network fallbacks sticky after failed attempts and retry timed-out startup control calls once on the fallback route, so `deleteWebhook` IPv6 stalls no longer trigger slow multi-account retry storms. Fixes #73255. Thanks @ttomiczek and @sktbrd.
|
||||
- Gateway/models: merge explicit `models.providers.*.models` rows into the Gateway model catalog with normalized provider/model dedupe, and use normalized image-capability lookup so custom vision models keep native image attachments even when Pi discovery omits them or model ID casing differs. Fixes #64213 and #65165. Thanks @billonese and @202233a.
|
||||
- Gateway/reload: publish canonical post-write source config to in-process reloaders so simple config saves no longer create phantom plugin diffs or trigger unnecessary Gateway restarts. (#73267) Thanks @szsip239.
|
||||
- Export/session: keep inline export HTML scripts and vendor libraries injected after template formatting so generated session exports open with the app code, markdown renderer, and syntax highlighter present. Fixes #41862 and #49957; carries forward #41861 and #68947. Thanks @briannewman, @martenzi, and @armanddp.
|
||||
- Agents/ACPX: stage the patched Claude ACP adapter as an ACPX runtime dependency and route known Codex/Claude ACP commands through local wrappers, so Gateway runtime no longer depends on live `npx` adapter resolution. Fixes #73202. Thanks @joerod26.
|
||||
- Memory/compaction: let pre-compaction memory flush use an exact `agents.defaults.compaction.memoryFlush.model` override such as `ollama/qwen3:8b` without inheriting the active session fallback chain, so local housekeeping can avoid paid conversation models. Fixes #53772. Thanks @limen96.
|
||||
|
||||
@@ -2442,6 +2442,28 @@ export async function writeConfigFile(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Re-read the freshly persisted file so the sourceConfig we publish matches
|
||||
// exactly what readConfigFileSnapshot() will produce when the file-watcher
|
||||
// path next picks up an external edit. Without this, the in-process write
|
||||
// path emits `nextCfg` (the pre-write source merge) while the file-watcher
|
||||
// path emits a sourceConfig that has additionally been shaped by include/
|
||||
// env resolution, legacy migration, and the shipped-plugin-install strip.
|
||||
// The two diverge on schema-derived defaults that the read pipeline adds
|
||||
// but `nextCfg` never sees, so the gateway reload pump's
|
||||
// currentCompareConfig drifts permanently from on-disk state and diffs out
|
||||
// phantom paths under plugins.entries.* on every save — incorrectly
|
||||
// triggering a `plugins`-scoped restart of the gateway for changes that
|
||||
// never touched any plugin entry.
|
||||
let canonicalSourceConfig: OpenClawConfig = nextCfg;
|
||||
try {
|
||||
const freshSnapshot = await io.readConfigFileSnapshot();
|
||||
if (freshSnapshot.exists && freshSnapshot.valid) {
|
||||
canonicalSourceConfig = freshSnapshot.sourceConfig;
|
||||
}
|
||||
} catch {
|
||||
// Best-effort; fall back to nextCfg so a transient read failure does not
|
||||
// block the write notification.
|
||||
}
|
||||
const notifyCommittedWrite = () => {
|
||||
const currentRuntimeConfig = getRuntimeConfigSnapshotState();
|
||||
if (!currentRuntimeConfig) {
|
||||
@@ -2450,7 +2472,7 @@ export async function writeConfigFile(
|
||||
notifyRuntimeConfigWriteListeners(
|
||||
createRuntimeConfigWriteNotification({
|
||||
configPath: io.configPath,
|
||||
sourceConfig: nextCfg,
|
||||
sourceConfig: canonicalSourceConfig,
|
||||
runtimeConfig: currentRuntimeConfig,
|
||||
persistedHash: writeResult.persistedHash,
|
||||
afterWrite: options.afterWrite,
|
||||
@@ -2460,7 +2482,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: nextCfg,
|
||||
nextSourceConfig: canonicalSourceConfig,
|
||||
hadRuntimeSnapshot,
|
||||
hadBothSnapshots,
|
||||
loadFreshConfig: () => io.loadConfig(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
createConfigIO,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
registerConfigWriteListener,
|
||||
resetConfigRuntimeState,
|
||||
setRuntimeConfigSnapshot,
|
||||
@@ -887,4 +888,92 @@ describe("config io write", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies in-process reloaders with canonical post-write source config", async () => {
|
||||
mockLoadPluginManifestRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "demo",
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
rootDir: "/tmp/openclaw-test-demo",
|
||||
source: "/tmp/openclaw-test-demo/index.ts",
|
||||
manifestPath: "/tmp/openclaw-test-demo/openclaw.plugin.json",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: { type: "string", default: "auto" },
|
||||
},
|
||||
additionalProperties: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies PluginManifestRegistry);
|
||||
|
||||
await withSuiteHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
const previousConfigPath = process.env.OPENCLAW_CONFIG_PATH;
|
||||
process.env.OPENCLAW_CONFIG_PATH = configPath;
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
const sourceConfig = {
|
||||
gateway: { mode: "local" },
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.4" } } },
|
||||
plugins: { entries: { demo: { enabled: true, config: {} } } },
|
||||
} satisfies ConfigFileSnapshot["sourceConfig"];
|
||||
await fs.writeFile(configPath, `${JSON.stringify(sourceConfig, null, 2)}\n`, "utf-8");
|
||||
const runtimeConfig = {
|
||||
...structuredClone(sourceConfig),
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { mode: "auto" } },
|
||||
},
|
||||
},
|
||||
} satisfies ConfigFileSnapshot["config"];
|
||||
const observedSources: unknown[] = [];
|
||||
const unsubscribe = registerConfigWriteListener((event) => {
|
||||
observedSources.push(event.sourceConfig);
|
||||
});
|
||||
|
||||
try {
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
|
||||
await writeConfigFile({
|
||||
...runtimeConfig,
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter/anthropic/claude-sonnet-4.6" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const postWriteSnapshot = await createConfigIO({
|
||||
env: { OPENCLAW_CONFIG_PATH: configPath, VITEST: "true" } as NodeJS.ProcessEnv,
|
||||
homedir: () => home,
|
||||
logger: silentLogger,
|
||||
}).readConfigFileSnapshot();
|
||||
|
||||
expect(postWriteSnapshot.valid).toBe(true);
|
||||
expect(observedSources).toEqual([postWriteSnapshot.sourceConfig]);
|
||||
expect(getRuntimeConfigSourceSnapshot()).toEqual(postWriteSnapshot.sourceConfig);
|
||||
expect(postWriteSnapshot.sourceConfig.meta?.lastTouchedAt).toEqual(expect.any(String));
|
||||
expect(postWriteSnapshot.sourceConfig.plugins?.entries?.demo?.config).toEqual({});
|
||||
} finally {
|
||||
unsubscribe();
|
||||
mockLoadPluginManifestRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [],
|
||||
} satisfies PluginManifestRegistry);
|
||||
if (previousConfigPath === undefined) {
|
||||
delete process.env.OPENCLAW_CONFIG_PATH;
|
||||
} else {
|
||||
process.env.OPENCLAW_CONFIG_PATH = previousConfigPath;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user