Skip to content

Commit 35cd2af

Browse files
LLagoon3altaywtf
andauthored
Expose reload kind in config schema lookup (#81612)
Merged via squash. Prepared head SHA: 9517cfa Co-authored-by: LLagoon3 <115124830+LLagoon3@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf
1 parent 6a5a135 commit 35cd2af

10 files changed

Lines changed: 80 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
3838
- 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.
3939
- Gateway/performance: add `pnpm test:restart:gateway` benchmark tooling for repeated restart readiness, downtime, trace, and resource-slope evidence. (#83299) Thanks @samzong.
4040
- 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.
41+
- 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.
4142

4243
### Fixes
4344

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2757,19 +2757,22 @@ public struct ConfigSchemaResponse: Codable, Sendable {
27572757
public struct ConfigSchemaLookupResult: Codable, Sendable {
27582758
public let path: String
27592759
public let schema: AnyCodable
2760+
public let reloadkind: AnyCodable?
27602761
public let hint: [String: AnyCodable]?
27612762
public let hintpath: String?
27622763
public let children: [[String: AnyCodable]]
27632764

27642765
public init(
27652766
path: String,
27662767
schema: AnyCodable,
2768+
reloadkind: AnyCodable?,
27672769
hint: [String: AnyCodable]?,
27682770
hintpath: String?,
27692771
children: [[String: AnyCodable]])
27702772
{
27712773
self.path = path
27722774
self.schema = schema
2775+
self.reloadkind = reloadkind
27732776
self.hint = hint
27742777
self.hintpath = hintpath
27752778
self.children = children
@@ -2778,6 +2781,7 @@ public struct ConfigSchemaLookupResult: Codable, Sendable {
27782781
private enum CodingKeys: String, CodingKey {
27792782
case path
27802783
case schema
2784+
case reloadkind = "reloadKind"
27812785
case hint
27822786
case hintpath = "hintPath"
27832787
case children

docs/gateway/protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
392392
- `config.patch` merges a partial config update.
393393
- `config.apply` validates + replaces the full config payload.
394394
- `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.
395-
- `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`.
395+
- `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`.
396396
- `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.
397397
- `update.status` returns the latest cached update restart sentinel, including the post-restart running version when available.
398398
- `wizard.start`, `wizard.next`, `wizard.status`, and `wizard.cancel` expose the onboarding wizard over WS RPC.

src/config/schema.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,28 @@ describe("config schema", () => {
663663
expect(schema?.properties).toBeUndefined();
664664
});
665665

666-
it("returns a shallow lookup schema with top-level composition for editing", () => {
666+
it("includes reload metadata when a resolver is provided", () => {
667+
const lookup = lookupConfigSchema(baseSchema, "gateway", (path) => {
668+
if (path === "gateway.channelHealthCheckMinutes") {
669+
return { kind: "hot" };
670+
}
671+
if (path.startsWith("gateway")) {
672+
return { kind: "restart" };
673+
}
674+
return { kind: "none" };
675+
});
676+
677+
expect(lookup?.reloadKind).toBe("restart");
678+
expect(
679+
lookup?.children.find((child) => child.path === "gateway.handshakeTimeoutMs")?.reloadKind,
680+
).toBe("restart");
681+
expect(
682+
lookup?.children.find((child) => child.path === "gateway.channelHealthCheckMinutes")
683+
?.reloadKind,
684+
).toBe("hot");
685+
});
686+
687+
it("returns a shallow lookup schema without nested composition keywords", () => {
667688
const lookup = lookupConfigSchema(baseSchema, "agents.list.0.runtime");
668689
expect(lookup?.path).toBe("agents.list.0.runtime");
669690
expect(lookup?.hintPath).toBe("agents.list[].runtime");

src/config/schema.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,25 @@ export type ConfigSchemaLookupChild = {
112112
type?: string | string[];
113113
required: boolean;
114114
hasChildren: boolean;
115+
reloadKind?: ConfigSchemaReloadKind;
115116
hint?: ConfigUiHint;
116117
hintPath?: string;
117118
};
118119

120+
export type ConfigSchemaReloadKind = "restart" | "hot" | "none";
121+
122+
export type ConfigSchemaReloadMetadata = {
123+
kind: ConfigSchemaReloadKind;
124+
};
125+
126+
export type ConfigSchemaReloadMetadataResolver = (
127+
path: string,
128+
) => ConfigSchemaReloadMetadata | null | undefined;
129+
119130
export type ConfigSchemaLookupResult = {
120131
path: string;
121132
schema: JsonSchemaNode;
133+
reloadKind?: ConfigSchemaReloadKind;
122134
hint?: ConfigUiHint;
123135
hintPath?: string;
124136
children: ConfigSchemaLookupChild[];
@@ -750,19 +762,22 @@ function buildLookupChildren(
750762
schema: JsonSchemaObject,
751763
path: string,
752764
uiHints: ConfigUiHints,
765+
resolveReloadMetadata?: ConfigSchemaReloadMetadataResolver,
753766
): ConfigSchemaLookupChild[] {
754767
const children: ConfigSchemaLookupChild[] = [];
755768
const required = new Set(schema.required ?? []);
756769

757770
const pushChild = (key: string, childSchema: JsonSchemaObject, isRequired: boolean) => {
758771
const childPath = path ? `${path}.${key}` : key;
759772
const resolvedHint = resolveUiHintMatch(uiHints, childPath);
773+
const reloadMetadata = resolveReloadMetadata?.(childPath);
760774
children.push({
761775
key,
762776
path: childPath,
763777
type: childSchema.type,
764778
required: isRequired,
765779
hasChildren: schemaHasChildren(childSchema),
780+
reloadKind: reloadMetadata?.kind,
766781
hint: resolvedHint?.hint,
767782
hintPath: resolvedHint?.path,
768783
});
@@ -788,6 +803,7 @@ function buildLookupChildren(
788803
export function lookupConfigSchema(
789804
response: ConfigSchemaResponse,
790805
path: string,
806+
resolveReloadMetadata?: ConfigSchemaReloadMetadataResolver,
791807
): ConfigSchemaLookupResult | null {
792808
const wantsRoot = path.trim() === ".";
793809
const normalizedPath = normalizeLookupPath(path);
@@ -812,11 +828,18 @@ export function lookupConfigSchema(
812828
}
813829

814830
const resolvedHint = resolveUiHintMatch(response.uiHints, normalizedPath);
831+
const reloadMetadata = resolveReloadMetadata?.(normalizedPath);
815832
return {
816833
path: wantsRoot ? "." : normalizedPath,
817834
schema: stripSchemaForLookup(current),
835+
reloadKind: reloadMetadata?.kind,
818836
hint: resolvedHint?.hint,
819837
hintPath: resolvedHint?.path,
820-
children: buildLookupChildren(current, wantsRoot ? "" : normalizedPath, response.uiHints),
838+
children: buildLookupChildren(
839+
current,
840+
wantsRoot ? "" : normalizedPath,
841+
response.uiHints,
842+
resolveReloadMetadata,
843+
),
821844
};
822845
}

src/gateway/config-reload-plan.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ type ReloadRule = {
3030
actions?: ReloadAction[];
3131
};
3232

33+
export type ConfigReloadMetadata = {
34+
kind: ReloadRule["kind"];
35+
};
36+
3337
type ReloadAction =
3438
| "reload-hooks"
3539
| "restart-gmail-watcher"
@@ -222,6 +226,13 @@ function matchRule(path: string): ReloadRule | null {
222226
return null;
223227
}
224228

229+
export function resolveConfigReloadMetadata(path: string): ConfigReloadMetadata {
230+
if (isPluginInstallTimestampPath(path)) {
231+
return { kind: "none" };
232+
}
233+
return { kind: matchRule(path)?.kind ?? "restart" };
234+
}
235+
225236
function isPluginInstallTimestampPath(path: string): boolean {
226237
// Legacy compatibility only: new plugin install metadata lives in the
227238
// managed plugin index, but old config writes may still touch this path.

src/gateway/config-reload.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
type GatewayReloadPlan,
2525
listPluginInstallTimestampMetadataPaths,
2626
listPluginInstallWholeRecordPaths,
27+
resolveConfigReloadMetadata,
2728
resolveGatewayReloadSettings,
2829
shouldInvalidateSkillsSnapshotForPaths,
2930
startGatewayConfigReloader,
@@ -298,6 +299,12 @@ describe("buildGatewayReloadPlan", () => {
298299
"plugins.installs.lossless-claw.resolvedAt",
299300
"plugins.installs.lossless-claw.installedAt",
300301
]);
302+
expect(resolveConfigReloadMetadata("plugins.installs.lossless-claw.resolvedAt").kind).toBe(
303+
"none",
304+
);
305+
expect(resolveConfigReloadMetadata("plugins.installs.lossless-claw.installedAt").kind).toBe(
306+
"none",
307+
);
301308
});
302309

303310
it("restarts for whole-record plugin install changes", () => {

src/gateway/config-reload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
buildGatewayReloadPlan,
1515
listPluginInstallTimestampMetadataPaths,
1616
listPluginInstallWholeRecordPaths,
17+
resolveConfigReloadMetadata,
1718
type GatewayReloadPlan,
1819
} from "./config-reload-plan.js";
1920
import { resolveGatewayReloadSettings } from "./config-reload-settings.js";
@@ -23,6 +24,7 @@ export {
2324
diffConfigPaths,
2425
listPluginInstallTimestampMetadataPaths,
2526
listPluginInstallWholeRecordPaths,
27+
resolveConfigReloadMetadata,
2628
resolveGatewayReloadSettings,
2729
};
2830
export type { ChannelKind, GatewayReloadPlan } from "./config-reload-plan.js";

src/gateway/protocol/schema/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ export const ConfigSchemaLookupChildSchema = Type.Object(
9797
type: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
9898
required: Type.Boolean(),
9999
hasChildren: Type.Boolean(),
100+
reloadKind: Type.Optional(
101+
Type.Union([Type.Literal("restart"), Type.Literal("hot"), Type.Literal("none")]),
102+
),
100103
hint: Type.Optional(ConfigUiHintSchema),
101104
hintPath: Type.Optional(Type.String()),
102105
},
@@ -107,6 +110,9 @@ export const ConfigSchemaLookupResultSchema = Type.Object(
107110
{
108111
path: NonEmptyString,
109112
schema: Type.Unknown(),
113+
reloadKind: Type.Optional(
114+
Type.Union([Type.Literal("restart"), Type.Literal("hot"), Type.Literal("none")]),
115+
),
110116
hint: Type.Optional(ConfigUiHintSchema),
111117
hintPath: Type.Optional(Type.String()),
112118
children: Type.Array(ConfigSchemaLookupChildSchema),

src/gateway/server-methods/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
type PreparedSecretsRuntimeSnapshot,
2424
} from "../../secrets/runtime.js";
2525
import { diffConfigPaths } from "../config-diff.js";
26+
import { resolveConfigReloadMetadata } from "../config-reload-plan.js";
2627
import {
2728
formatControlPlaneActor,
2829
resolveControlPlaneActor,
@@ -313,7 +314,7 @@ export const configHandlers: GatewayRequestHandlers = {
313314
}
314315
const path = (params as { path: string }).path;
315316
const schema = loadSchemaWithPlugins();
316-
const result = lookupConfigSchema(schema, path);
317+
const result = lookupConfigSchema(schema, path, resolveConfigReloadMetadata);
317318
if (!result) {
318319
respond(
319320
false,

0 commit comments

Comments
 (0)