mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user