refactor(plugin-sdk): split runtime helper seams

This commit is contained in:
Peter Steinberger
2026-04-04 08:53:12 +01:00
parent 470898b5e1
commit edfaa01d1d
57 changed files with 605 additions and 333 deletions

View File

@@ -41,6 +41,15 @@ import { applyMergePatch } from "./merge-patch.js";
import { resolveConfigPath, resolveStateDir } from "./paths.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
import { applyConfigOverrides } from "./runtime-overrides.js";
import {
clearRuntimeConfigSnapshot as clearRuntimeConfigSnapshotState,
getRuntimeConfigSnapshot as getRuntimeConfigSnapshotState,
getRuntimeConfigSnapshotRefreshHandler,
getRuntimeConfigSourceSnapshot as getRuntimeConfigSourceSnapshotState,
resetConfigRuntimeState as resetConfigRuntimeStateState,
setRuntimeConfigSnapshot as setRuntimeConfigSnapshotState,
setRuntimeConfigSnapshotRefreshHandler as setRuntimeConfigSnapshotRefreshHandlerState,
} from "./runtime-snapshot.js";
import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
import {
validateConfigObjectRawWithPlugins,
@@ -48,6 +57,15 @@ import {
} from "./validation.js";
import { shouldWarnOnTouchedVersion } from "./version.js";
export {
clearRuntimeConfigSnapshotState as clearRuntimeConfigSnapshot,
getRuntimeConfigSnapshotState as getRuntimeConfigSnapshot,
getRuntimeConfigSourceSnapshotState as getRuntimeConfigSourceSnapshot,
resetConfigRuntimeStateState as resetConfigRuntimeState,
setRuntimeConfigSnapshotState as setRuntimeConfigSnapshot,
setRuntimeConfigSnapshotRefreshHandlerState as setRuntimeConfigSnapshotRefreshHandler,
};
// Re-export for backwards compatibility
export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
export { MissingEnvVarError } from "./env-substitution.js";
@@ -228,15 +246,6 @@ export type ReadConfigFileSnapshotForWriteResult = {
writeOptions: ConfigWriteOptions;
};
export type RuntimeConfigSnapshotRefreshParams = {
sourceConfig: OpenClawConfig;
};
export type RuntimeConfigSnapshotRefreshHandler = {
refresh: (params: RuntimeConfigSnapshotRefreshParams) => boolean | Promise<boolean>;
clearOnRefreshFailure?: () => void;
};
export type ConfigWriteNotification = {
configPath: string;
sourceConfig: OpenClawConfig;
@@ -2371,9 +2380,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
const AUTO_OWNER_DISPLAY_SECRET_BY_PATH = new Map<string, string>();
const AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT = new Set<string>();
const AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED = new Set<string>();
let runtimeConfigSnapshot: OpenClawConfig | null = null;
let runtimeConfigSourceSnapshot: OpenClawConfig | null = null;
let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null;
const configWriteListeners = new Set<(event: ConfigWriteNotification) => void>();
function notifyConfigWriteListeners(event: ConfigWriteNotification): void {
@@ -2399,31 +2405,6 @@ export function registerConfigWriteListener(
};
}
export function setRuntimeConfigSnapshot(
config: OpenClawConfig,
sourceConfig?: OpenClawConfig,
): void {
runtimeConfigSnapshot = config;
runtimeConfigSourceSnapshot = sourceConfig ?? null;
}
export function resetConfigRuntimeState(): void {
runtimeConfigSnapshot = null;
runtimeConfigSourceSnapshot = null;
}
export function clearRuntimeConfigSnapshot(): void {
resetConfigRuntimeState();
}
export function getRuntimeConfigSnapshot(): OpenClawConfig | null {
return runtimeConfigSnapshot;
}
export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null {
return runtimeConfigSourceSnapshot;
}
function isCompatibleTopLevelRuntimeProjectionShape(params: {
runtimeSnapshot: OpenClawConfig;
candidate: OpenClawConfig;
@@ -2454,6 +2435,8 @@ function isCompatibleTopLevelRuntimeProjectionShape(params: {
}
export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig): OpenClawConfig {
const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState();
if (!runtimeConfigSnapshot || !runtimeConfigSourceSnapshot) {
return config;
}
@@ -2476,13 +2459,8 @@ export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig):
return coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot, runtimePatch));
}
export function setRuntimeConfigSnapshotRefreshHandler(
refreshHandler: RuntimeConfigSnapshotRefreshHandler | null,
): void {
runtimeConfigSnapshotRefreshHandler = refreshHandler;
}
export function loadConfig(): OpenClawConfig {
const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
if (runtimeConfigSnapshot) {
return runtimeConfigSnapshot;
}
@@ -2490,8 +2468,8 @@ export function loadConfig(): OpenClawConfig {
// First successful load becomes the process snapshot. Long-lived runtimes
// should swap this snapshot via explicit reload/watcher paths instead of
// reparsing openclaw.json on hot code paths.
setRuntimeConfigSnapshot(config);
return runtimeConfigSnapshot ?? config;
setRuntimeConfigSnapshotState(config);
return getRuntimeConfigSnapshotState() ?? config;
}
export function getRuntimeConfig(): OpenClawConfig {
@@ -2525,6 +2503,8 @@ export async function writeConfigFile(
): Promise<void> {
const io = createConfigIO();
let nextCfg = cfg;
const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState();
const hadRuntimeSnapshot = Boolean(runtimeConfigSnapshot);
const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot);
if (hadBothSnapshots) {
@@ -2538,20 +2518,21 @@ export async function writeConfigFile(
unsetPaths: options.unsetPaths,
});
const notifyCommittedWrite = () => {
if (!runtimeConfigSnapshot) {
const currentRuntimeConfig = getRuntimeConfigSnapshotState();
if (!currentRuntimeConfig) {
return;
}
notifyConfigWriteListeners({
configPath: io.configPath,
sourceConfig: nextCfg,
runtimeConfig: runtimeConfigSnapshot,
runtimeConfig: currentRuntimeConfig,
persistedHash: writeResult.persistedHash,
writtenAtMs: Date.now(),
});
};
// Keep the last-known-good runtime snapshot active until the specialized refresh path
// succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh.
const refreshHandler = runtimeConfigSnapshotRefreshHandler;
const refreshHandler = getRuntimeConfigSnapshotRefreshHandler();
if (refreshHandler) {
try {
const refreshed = await refreshHandler.refresh({ sourceConfig: nextCfg });
@@ -2576,16 +2557,16 @@ export async function writeConfigFile(
// Refresh both snapshots from disk atomically so follow-up reads get normalized config and
// subsequent writes still get secret-preservation merge-patch (hadBothSnapshots stays true).
const fresh = io.loadConfig();
setRuntimeConfigSnapshot(fresh, nextCfg);
setRuntimeConfigSnapshotState(fresh, nextCfg);
notifyCommittedWrite();
return;
}
if (hadRuntimeSnapshot) {
const fresh = io.loadConfig();
setRuntimeConfigSnapshot(fresh);
setRuntimeConfigSnapshotState(fresh);
notifyCommittedWrite();
return;
}
setRuntimeConfigSnapshot(io.loadConfig());
setRuntimeConfigSnapshotState(io.loadConfig());
notifyCommittedWrite();
}

View File

@@ -31,7 +31,12 @@ function buildDefaultTableModes(): Map<string, MarkdownTableMode> {
);
}
export const DEFAULT_TABLE_MODES = buildDefaultTableModes();
let cachedDefaultTableModes: Map<string, MarkdownTableMode> | null = null;
function getDefaultTableModes(): Map<string, MarkdownTableMode> {
cachedDefaultTableModes ??= buildDefaultTableModes();
return cachedDefaultTableModes;
}
const isMarkdownTableMode = (value: unknown): value is MarkdownTableMode =>
value === "off" || value === "bullets" || value === "code" || value === "block";
@@ -62,7 +67,7 @@ export function resolveMarkdownTableMode(params: {
accountId?: string | null;
}): MarkdownTableMode {
const channel = normalizeChannelId(params.channel);
const defaultMode = channel ? (DEFAULT_TABLE_MODES.get(channel) ?? "code") : "code";
const defaultMode = channel ? (getDefaultTableModes().get(channel) ?? "code") : "code";
if (!channel || !params.cfg) {
return defaultMode;
}

View File

@@ -0,0 +1,49 @@
import type { OpenClawConfig } from "./types.js";
export type RuntimeConfigSnapshotRefreshParams = {
sourceConfig: OpenClawConfig;
};
export type RuntimeConfigSnapshotRefreshHandler = {
refresh: (params: RuntimeConfigSnapshotRefreshParams) => boolean | Promise<boolean>;
clearOnRefreshFailure?: () => void;
};
let runtimeConfigSnapshot: OpenClawConfig | null = null;
let runtimeConfigSourceSnapshot: OpenClawConfig | null = null;
let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null;
export function setRuntimeConfigSnapshot(
config: OpenClawConfig,
sourceConfig?: OpenClawConfig,
): void {
runtimeConfigSnapshot = config;
runtimeConfigSourceSnapshot = sourceConfig ?? null;
}
export function resetConfigRuntimeState(): void {
runtimeConfigSnapshot = null;
runtimeConfigSourceSnapshot = null;
}
export function clearRuntimeConfigSnapshot(): void {
resetConfigRuntimeState();
}
export function getRuntimeConfigSnapshot(): OpenClawConfig | null {
return runtimeConfigSnapshot;
}
export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null {
return runtimeConfigSourceSnapshot;
}
export function setRuntimeConfigSnapshotRefreshHandler(
refreshHandler: RuntimeConfigSnapshotRefreshHandler | null,
): void {
runtimeConfigSnapshotRefreshHandler = refreshHandler;
}
export function getRuntimeConfigSnapshotRefreshHandler(): RuntimeConfigSnapshotRefreshHandler | null {
return runtimeConfigSnapshotRefreshHandler;
}