Expose reload kind in config schema lookup (#81612)

Merged via squash.

Prepared head SHA: 9517cfa718
Co-authored-by: LLagoon3 <115124830+LLagoon3@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
LLagoon3
2026-05-18 22:39:12 +09:00
committed by GitHub
parent 6a5a1353c7
commit 35cd2af159
10 changed files with 80 additions and 4 deletions
+1
View File
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
- QA-Lab: extend the personal-agent benchmark pack with a local task followthrough scenario for proof-backed pending, blocked, and done status reporting. Thanks @iFiras-Max1.
- Gateway/performance: add `pnpm test:restart:gateway` benchmark tooling for repeated restart readiness, downtime, trace, and resource-slope evidence. (#83299) Thanks @samzong.
- Android: switch Talk Mode to realtime Gateway relay voice sessions with streaming mic input, realtime audio playback, tool-result bridging, and on-screen transcripts. (#83130) Thanks @sliekens.
- Gateway/config: expose config lookup reload metadata so tools can distinguish restart-required, hot-reloadable, and no-op fields before applying config edits. Fixes #81409. (#81612) Thanks @LLagoon3.
### Fixes
@@ -2757,6 +2757,7 @@ public struct ConfigSchemaResponse: Codable, Sendable {
public struct ConfigSchemaLookupResult: Codable, Sendable {
public let path: String
public let schema: AnyCodable
public let reloadkind: AnyCodable?
public let hint: [String: AnyCodable]?
public let hintpath: String?
public let children: [[String: AnyCodable]]
@@ -2764,12 +2765,14 @@ public struct ConfigSchemaLookupResult: Codable, Sendable {
public init(
path: String,
schema: AnyCodable,
reloadkind: AnyCodable?,
hint: [String: AnyCodable]?,
hintpath: String?,
children: [[String: AnyCodable]])
{
self.path = path
self.schema = schema
self.reloadkind = reloadkind
self.hint = hint
self.hintpath = hintpath
self.children = children
@@ -2778,6 +2781,7 @@ public struct ConfigSchemaLookupResult: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case path
case schema
case reloadkind = "reloadKind"
case hint
case hintpath = "hintPath"
case children
+1 -1
View File
@@ -392,7 +392,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
- `config.patch` merges a partial config update.
- `config.apply` validates + replaces the full config payload.
- `config.schema` returns the live config schema payload used by Control UI and CLI tooling: schema, `uiHints`, version, and generation metadata, including plugin + channel schema metadata when the runtime can load it. The schema includes field `title` / `description` metadata derived from the same labels and help text used by the UI, including nested object, wildcard, array-item, and `anyOf` / `oneOf` / `allOf` composition branches when matching field documentation exists.
- `config.schema.lookup` returns a path-scoped lookup payload for one config path: normalized path, a shallow schema node, matched hint + `hintPath`, and immediate child summaries for UI/CLI drill-down. Lookup schema nodes keep the user-facing docs and common validation fields (`title`, `description`, `type`, `enum`, `const`, `format`, `pattern`, numeric/string/array/object bounds, and flags like `additionalProperties`, `deprecated`, `readOnly`, `writeOnly`). Child summaries expose `key`, normalized `path`, `type`, `required`, `hasChildren`, plus the matched `hint` / `hintPath`.
- `config.schema.lookup` returns a path-scoped lookup payload for one config path: normalized path, a shallow schema node, matched hint + `hintPath`, optional `reloadKind`, and immediate child summaries for UI/CLI drill-down. `reloadKind` is one of `restart`, `hot`, or `none` and mirrors the Gateway config reload planner for the requested path. Lookup schema nodes keep the user-facing docs and common validation fields (`title`, `description`, `type`, `enum`, `const`, `format`, `pattern`, numeric/string/array/object bounds, and flags like `additionalProperties`, `deprecated`, `readOnly`, `writeOnly`). Child summaries expose `key`, normalized `path`, `type`, `required`, `hasChildren`, optional `reloadKind`, plus the matched `hint` / `hintPath`.
- `update.run` runs the gateway update flow and schedules a restart only when the update itself succeeded; callers with a session can include `continuationMessage` so startup resumes one follow-up agent turn through the restart continuation queue. Package-manager updates from the control plane use a detached managed-service handoff instead of replacing the package tree inside the live Gateway. A started handoff returns `ok: true` with `result.reason: "managed-service-handoff-started"` and `handoff.status: "started"`; unavailable or failed handoffs return `ok: false` with `managed-service-handoff-unavailable` or `managed-service-handoff-failed`, plus `handoff.command` when a manual shell update is required. During a started handoff, the restart sentinel may briefly report `stats.reason: "restart-health-pending"`; the continuation is delayed until the CLI verifies the restarted Gateway and writes the final `ok` sentinel.
- `update.status` returns the latest cached update restart sentinel, including the post-restart running version when available.
- `wizard.start`, `wizard.next`, `wizard.status`, and `wizard.cancel` expose the onboarding wizard over WS RPC.
+22 -1
View File
@@ -663,7 +663,28 @@ describe("config schema", () => {
expect(schema?.properties).toBeUndefined();
});
it("returns a shallow lookup schema with top-level composition for editing", () => {
it("includes reload metadata when a resolver is provided", () => {
const lookup = lookupConfigSchema(baseSchema, "gateway", (path) => {
if (path === "gateway.channelHealthCheckMinutes") {
return { kind: "hot" };
}
if (path.startsWith("gateway")) {
return { kind: "restart" };
}
return { kind: "none" };
});
expect(lookup?.reloadKind).toBe("restart");
expect(
lookup?.children.find((child) => child.path === "gateway.handshakeTimeoutMs")?.reloadKind,
).toBe("restart");
expect(
lookup?.children.find((child) => child.path === "gateway.channelHealthCheckMinutes")
?.reloadKind,
).toBe("hot");
});
it("returns a shallow lookup schema without nested composition keywords", () => {
const lookup = lookupConfigSchema(baseSchema, "agents.list.0.runtime");
expect(lookup?.path).toBe("agents.list.0.runtime");
expect(lookup?.hintPath).toBe("agents.list[].runtime");
+24 -1
View File
@@ -112,13 +112,25 @@ export type ConfigSchemaLookupChild = {
type?: string | string[];
required: boolean;
hasChildren: boolean;
reloadKind?: ConfigSchemaReloadKind;
hint?: ConfigUiHint;
hintPath?: string;
};
export type ConfigSchemaReloadKind = "restart" | "hot" | "none";
export type ConfigSchemaReloadMetadata = {
kind: ConfigSchemaReloadKind;
};
export type ConfigSchemaReloadMetadataResolver = (
path: string,
) => ConfigSchemaReloadMetadata | null | undefined;
export type ConfigSchemaLookupResult = {
path: string;
schema: JsonSchemaNode;
reloadKind?: ConfigSchemaReloadKind;
hint?: ConfigUiHint;
hintPath?: string;
children: ConfigSchemaLookupChild[];
@@ -750,6 +762,7 @@ function buildLookupChildren(
schema: JsonSchemaObject,
path: string,
uiHints: ConfigUiHints,
resolveReloadMetadata?: ConfigSchemaReloadMetadataResolver,
): ConfigSchemaLookupChild[] {
const children: ConfigSchemaLookupChild[] = [];
const required = new Set(schema.required ?? []);
@@ -757,12 +770,14 @@ function buildLookupChildren(
const pushChild = (key: string, childSchema: JsonSchemaObject, isRequired: boolean) => {
const childPath = path ? `${path}.${key}` : key;
const resolvedHint = resolveUiHintMatch(uiHints, childPath);
const reloadMetadata = resolveReloadMetadata?.(childPath);
children.push({
key,
path: childPath,
type: childSchema.type,
required: isRequired,
hasChildren: schemaHasChildren(childSchema),
reloadKind: reloadMetadata?.kind,
hint: resolvedHint?.hint,
hintPath: resolvedHint?.path,
});
@@ -788,6 +803,7 @@ function buildLookupChildren(
export function lookupConfigSchema(
response: ConfigSchemaResponse,
path: string,
resolveReloadMetadata?: ConfigSchemaReloadMetadataResolver,
): ConfigSchemaLookupResult | null {
const wantsRoot = path.trim() === ".";
const normalizedPath = normalizeLookupPath(path);
@@ -812,11 +828,18 @@ export function lookupConfigSchema(
}
const resolvedHint = resolveUiHintMatch(response.uiHints, normalizedPath);
const reloadMetadata = resolveReloadMetadata?.(normalizedPath);
return {
path: wantsRoot ? "." : normalizedPath,
schema: stripSchemaForLookup(current),
reloadKind: reloadMetadata?.kind,
hint: resolvedHint?.hint,
hintPath: resolvedHint?.path,
children: buildLookupChildren(current, wantsRoot ? "" : normalizedPath, response.uiHints),
children: buildLookupChildren(
current,
wantsRoot ? "" : normalizedPath,
response.uiHints,
resolveReloadMetadata,
),
};
}
+11
View File
@@ -30,6 +30,10 @@ type ReloadRule = {
actions?: ReloadAction[];
};
export type ConfigReloadMetadata = {
kind: ReloadRule["kind"];
};
type ReloadAction =
| "reload-hooks"
| "restart-gmail-watcher"
@@ -222,6 +226,13 @@ function matchRule(path: string): ReloadRule | null {
return null;
}
export function resolveConfigReloadMetadata(path: string): ConfigReloadMetadata {
if (isPluginInstallTimestampPath(path)) {
return { kind: "none" };
}
return { kind: matchRule(path)?.kind ?? "restart" };
}
function isPluginInstallTimestampPath(path: string): boolean {
// Legacy compatibility only: new plugin install metadata lives in the
// managed plugin index, but old config writes may still touch this path.
+7
View File
@@ -24,6 +24,7 @@ import {
type GatewayReloadPlan,
listPluginInstallTimestampMetadataPaths,
listPluginInstallWholeRecordPaths,
resolveConfigReloadMetadata,
resolveGatewayReloadSettings,
shouldInvalidateSkillsSnapshotForPaths,
startGatewayConfigReloader,
@@ -298,6 +299,12 @@ describe("buildGatewayReloadPlan", () => {
"plugins.installs.lossless-claw.resolvedAt",
"plugins.installs.lossless-claw.installedAt",
]);
expect(resolveConfigReloadMetadata("plugins.installs.lossless-claw.resolvedAt").kind).toBe(
"none",
);
expect(resolveConfigReloadMetadata("plugins.installs.lossless-claw.installedAt").kind).toBe(
"none",
);
});
it("restarts for whole-record plugin install changes", () => {
+2
View File
@@ -14,6 +14,7 @@ import {
buildGatewayReloadPlan,
listPluginInstallTimestampMetadataPaths,
listPluginInstallWholeRecordPaths,
resolveConfigReloadMetadata,
type GatewayReloadPlan,
} from "./config-reload-plan.js";
import { resolveGatewayReloadSettings } from "./config-reload-settings.js";
@@ -23,6 +24,7 @@ export {
diffConfigPaths,
listPluginInstallTimestampMetadataPaths,
listPluginInstallWholeRecordPaths,
resolveConfigReloadMetadata,
resolveGatewayReloadSettings,
};
export type { ChannelKind, GatewayReloadPlan } from "./config-reload-plan.js";
+6
View File
@@ -97,6 +97,9 @@ export const ConfigSchemaLookupChildSchema = Type.Object(
type: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
required: Type.Boolean(),
hasChildren: Type.Boolean(),
reloadKind: Type.Optional(
Type.Union([Type.Literal("restart"), Type.Literal("hot"), Type.Literal("none")]),
),
hint: Type.Optional(ConfigUiHintSchema),
hintPath: Type.Optional(Type.String()),
},
@@ -107,6 +110,9 @@ export const ConfigSchemaLookupResultSchema = Type.Object(
{
path: NonEmptyString,
schema: Type.Unknown(),
reloadKind: Type.Optional(
Type.Union([Type.Literal("restart"), Type.Literal("hot"), Type.Literal("none")]),
),
hint: Type.Optional(ConfigUiHintSchema),
hintPath: Type.Optional(Type.String()),
children: Type.Array(ConfigSchemaLookupChildSchema),
+2 -1
View File
@@ -23,6 +23,7 @@ import {
type PreparedSecretsRuntimeSnapshot,
} from "../../secrets/runtime.js";
import { diffConfigPaths } from "../config-diff.js";
import { resolveConfigReloadMetadata } from "../config-reload-plan.js";
import {
formatControlPlaneActor,
resolveControlPlaneActor,
@@ -313,7 +314,7 @@ export const configHandlers: GatewayRequestHandlers = {
}
const path = (params as { path: string }).path;
const schema = loadSchemaWithPlugins();
const result = lookupConfigSchema(schema, path);
const result = lookupConfigSchema(schema, path, resolveConfigReloadMetadata);
if (!result) {
respond(
false,