fix(status): keep diagnostics config reads read-only (#103252)

* fix: keep status config reads non-observing

Co-authored-by: Sascha Kuhlmann <coolmanns@users.noreply.github.com>

* fix: keep status preflight non-observing

* fix: keep status-all diagnosis non-observing

* chore: leave release changelog to maintainers

---------

Co-authored-by: Sascha Kuhlmann <coolmanns@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-07-10 05:16:44 +01:00
committed by GitHub
parent a2d5f32cef
commit 6fd9c1df69
10 changed files with 157 additions and 12 deletions

View File

@@ -186,6 +186,12 @@ describe("ensureConfigReady", () => {
}
});
it("keeps status config guard reads non-observing", async () => {
await runEnsureConfigReady(["status"]);
expect(readConfigFileSnapshotMock).toHaveBeenCalledWith({ observe: false });
});
it("runs doctor flow when lightweight startup detection finds legacy state", async () => {
const root = useTempOpenClawHome();
writeLegacyTaskSidecarMarker(root);
@@ -196,6 +202,7 @@ describe("ensureConfigReady", () => {
migrateState: true,
migrateLegacyConfig: false,
invalidConfigNote: false,
observe: false,
});
});
@@ -209,6 +216,7 @@ describe("ensureConfigReady", () => {
migrateState: true,
migrateLegacyConfig: false,
invalidConfigNote: false,
observe: false,
});
});

View File

@@ -168,7 +168,10 @@ function shouldRequireStartupMigrationCheckpoint(commandPath: string[]): boolean
);
}
async function getConfigSnapshot() {
async function getConfigSnapshot(options?: { observe: false }) {
if (options?.observe === false) {
return readConfigFileSnapshot(options);
}
// Tests often mutate config fixtures; caching can make those flaky.
if (process.env.VITEST === "true") {
return readConfigFileSnapshot();
@@ -193,6 +196,8 @@ export async function ensureConfigReady(params: {
beforeStateMigrations?: (snapshot?: ConfigFileSnapshot) => Promise<boolean>;
}): Promise<void> {
const commandPath = params.commandPath ?? [];
const commandName = commandPath[0];
const subcommandName = commandPath[1];
let preflightSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>> | null = null;
const shouldConsiderStateMigration = shouldMigrateStateFromPath(commandPath);
const requiresLegacyStateInput = shouldRunStateMigrationOnlyWithLegacyInputs(commandPath);
@@ -203,6 +208,7 @@ export async function ensureConfigReady(params: {
migrateState: true,
migrateLegacyConfig: false,
invalidConfigNote: false,
...(commandName === "status" ? { observe: false } : {}),
...(shouldRequireStartupMigrationCheckpoint(commandPath)
? { requireStartupMigrationCheckpoint: true }
: {}),
@@ -230,7 +236,11 @@ export async function ensureConfigReady(params: {
preflightSnapshot = await runStateMigrationPreflight();
}
let snapshot = preflightSnapshot ?? (await getConfigSnapshot());
// Status performs a second non-observing read for its materialized/source pair;
// keep the startup guard from recording config health before the command begins.
const configSnapshotOptions =
commandName === "status" ? ({ observe: false } as const) : undefined;
let snapshot = preflightSnapshot ?? (await getConfigSnapshot(configSnapshotOptions));
if (
!preflightSnapshot &&
!didRunDoctorConfigFlow &&
@@ -242,8 +252,6 @@ export async function ensureConfigReady(params: {
preflightSnapshot = await runStateMigrationPreflight();
snapshot = preflightSnapshot;
}
const commandName = commandPath[0];
const subcommandName = commandPath[1];
const isBareGatewayForegroundRun =
commandName === "gateway" && (subcommandName === undefined || subcommandName.trim() === "");
const isReadOnlyTaskStateCommand =