Skip to content

Commit 021d9fc

Browse files
committed
Expose config reload kind in schema lookup
1 parent a504cd0 commit 021d9fc

10 files changed

Lines changed: 74 additions & 4 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2687,19 +2687,22 @@ public struct ConfigSchemaResponse: Codable, Sendable {
26872687
public struct ConfigSchemaLookupResult: Codable, Sendable {
26882688
public let path: String
26892689
public let schema: AnyCodable
2690+
public let reloadkind: AnyCodable?
26902691
public let hint: [String: AnyCodable]?
26912692
public let hintpath: String?
26922693
public let children: [[String: AnyCodable]]
26932694

26942695
public init(
26952696
path: String,
26962697
schema: AnyCodable,
2698+
reloadkind: AnyCodable?,
26972699
hint: [String: AnyCodable]?,
26982700
hintpath: String?,
26992701
children: [[String: AnyCodable]])
27002702
{
27012703
self.path = path
27022704
self.schema = schema
2705+
self.reloadkind = reloadkind
27032706
self.hint = hint
27042707
self.hintpath = hintpath
27052708
self.children = children
@@ -2708,6 +2711,7 @@ public struct ConfigSchemaLookupResult: Codable, Sendable {
27082711
private enum CodingKeys: String, CodingKey {
27092712
case path
27102713
case schema
2714+
case reloadkind = "reloadKind"
27112715
case hint
27122716
case hintpath = "hintPath"
27132717
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 force a non-deferred, no-cooldown update restart after the package swap so the old Gateway process does not keep lazy-loading from a replaced `dist` tree.
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/agents/models.profiles.live.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ async function completeSimpleWithTimeout<TApi extends Api>(
545545
}
546546
}
547547

548-
function requireToolChoicePayload(payload: unknown): unknown | undefined {
548+
function requireToolChoicePayload(payload: unknown): unknown {
549549
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
550550
return undefined;
551551
}

src/config/schema.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,27 @@ describe("config schema", () => {
550550
expect(schema?.properties).toBeUndefined();
551551
});
552552

553+
it("includes reload metadata when a resolver is provided", () => {
554+
const lookup = lookupConfigSchema(baseSchema, "gateway", (path) => {
555+
if (path === "gateway.channelHealthCheckMinutes") {
556+
return { kind: "hot" };
557+
}
558+
if (path.startsWith("gateway")) {
559+
return { kind: "restart" };
560+
}
561+
return { kind: "none" };
562+
});
563+
564+
expect(lookup?.reloadKind).toBe("restart");
565+
expect(
566+
lookup?.children.find((child) => child.path === "gateway.handshakeTimeoutMs")?.reloadKind,
567+
).toBe("restart");
568+
expect(
569+
lookup?.children.find((child) => child.path === "gateway.channelHealthCheckMinutes")
570+
?.reloadKind,
571+
).toBe("hot");
572+
});
573+
553574
it("returns a shallow lookup schema without nested composition keywords", () => {
554575
const lookup = lookupConfigSchema(baseSchema, "agents.list.0.runtime");
555576
expect(lookup?.path).toBe("agents.list.0.runtime");

src/config/schema.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,25 @@ export type ConfigSchemaLookupChild = {
107107
type?: string | string[];
108108
required: boolean;
109109
hasChildren: boolean;
110+
reloadKind?: ConfigSchemaReloadKind;
110111
hint?: ConfigUiHint;
111112
hintPath?: string;
112113
};
113114

115+
export type ConfigSchemaReloadKind = "restart" | "hot" | "none";
116+
117+
export type ConfigSchemaReloadMetadata = {
118+
kind: ConfigSchemaReloadKind;
119+
};
120+
121+
export type ConfigSchemaReloadMetadataResolver = (
122+
path: string,
123+
) => ConfigSchemaReloadMetadata | null | undefined;
124+
114125
export type ConfigSchemaLookupResult = {
115126
path: string;
116127
schema: JsonSchemaNode;
128+
reloadKind?: ConfigSchemaReloadKind;
117129
hint?: ConfigUiHint;
118130
hintPath?: string;
119131
children: ConfigSchemaLookupChild[];
@@ -710,19 +722,22 @@ function buildLookupChildren(
710722
schema: JsonSchemaObject,
711723
path: string,
712724
uiHints: ConfigUiHints,
725+
resolveReloadMetadata?: ConfigSchemaReloadMetadataResolver,
713726
): ConfigSchemaLookupChild[] {
714727
const children: ConfigSchemaLookupChild[] = [];
715728
const required = new Set(schema.required ?? []);
716729

717730
const pushChild = (key: string, childSchema: JsonSchemaObject, isRequired: boolean) => {
718731
const childPath = path ? `${path}.${key}` : key;
719732
const resolvedHint = resolveUiHintMatch(uiHints, childPath);
733+
const reloadMetadata = resolveReloadMetadata?.(childPath);
720734
children.push({
721735
key,
722736
path: childPath,
723737
type: childSchema.type,
724738
required: isRequired,
725739
hasChildren: schemaHasChildren(childSchema),
740+
reloadKind: reloadMetadata?.kind,
726741
hint: resolvedHint?.hint,
727742
hintPath: resolvedHint?.path,
728743
});
@@ -748,6 +763,7 @@ function buildLookupChildren(
748763
export function lookupConfigSchema(
749764
response: ConfigSchemaResponse,
750765
path: string,
766+
resolveReloadMetadata?: ConfigSchemaReloadMetadataResolver,
751767
): ConfigSchemaLookupResult | null {
752768
const normalizedPath = normalizeLookupPath(path);
753769
if (!normalizedPath) {
@@ -771,11 +787,13 @@ export function lookupConfigSchema(
771787
}
772788

773789
const resolvedHint = resolveUiHintMatch(response.uiHints, normalizedPath);
790+
const reloadMetadata = resolveReloadMetadata?.(normalizedPath);
774791
return {
775792
path: normalizedPath,
776793
schema: stripSchemaForLookup(current),
794+
reloadKind: reloadMetadata?.kind,
777795
hint: resolvedHint?.hint,
778796
hintPath: resolvedHint?.path,
779-
children: buildLookupChildren(current, normalizedPath, response.uiHints),
797+
children: buildLookupChildren(current, normalizedPath, response.uiHints, resolveReloadMetadata),
780798
};
781799
}

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"
@@ -221,6 +225,13 @@ function matchRule(path: string): ReloadRule | null {
221225
return null;
222226
}
223227

228+
export function resolveConfigReloadMetadata(path: string): ConfigReloadMetadata {
229+
if (isPluginInstallTimestampPath(path)) {
230+
return { kind: "none" };
231+
}
232+
return { kind: matchRule(path)?.kind ?? "restart" };
233+
}
234+
224235
function isPluginInstallTimestampPath(path: string): boolean {
225236
// Legacy compatibility only: new plugin install metadata lives in the
226237
// 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,
@@ -289,7 +290,7 @@ export const configHandlers: GatewayRequestHandlers = {
289290
}
290291
const path = (params as { path: string }).path;
291292
const schema = loadSchemaWithPlugins();
292-
const result = lookupConfigSchema(schema, path);
293+
const result = lookupConfigSchema(schema, path, resolveConfigReloadMetadata);
293294
if (!result) {
294295
respond(
295296
false,

0 commit comments

Comments
 (0)