Skip to content

Commit 2d1446f

Browse files
stilrmyjalehman
andauthored
[fix] fix startup summary model banner resolution (#305)
* fix startup summary model banner resolution * fix: use runtime config for startup model resolution Keep registration-time plugin overrides from api.pluginConfig, but always source the OpenClaw config surface from the best validated runtime config available. This fixes the startup ordering where pluginConfig is ready before api.config, so the compaction banner and summarizer fallback both see the configured default model. Also add a regression test for that ordering, relax the provider-config loadConfig call-count assertion to match the earlier registration lookup, and add a patch changeset for the user-visible startup behavior fix. Regeneration-Prompt: | Address the PR review finding that registration-time config resolution still broke when OpenClaw populated api.pluginConfig before api.config. Preserve the intended behavior of preferring api.pluginConfig for lossless-claw-specific overrides, but stop coupling that to the top-level OpenClaw config used for default and compaction model discovery. Update the registration tests to cover the startup order where runtime.config.loadConfig() contains the default model while api.config is still empty, and verify both the startup banner and resolveModel() pick up that default. Because registration now consults runtime config earlier, adjust any overly strict tests that asserted an exact loadConfig call count. Since the startup banner output changes for users, include a patch changeset. * test: align rebased startup-banner assertions Current main logs startup banner messages through the plugin logger info path instead of console.error. Update the rebased PR 305 registration-order tests to assert the current logging surface so the branch reflects the intended startup-model fix and the targeted test run stays green. Regeneration-Prompt: | Rebase the old PR 305 startup-model fix onto current origin/main and resolve conflicts without losing main's newer registration diagnostics and logger plumbing. After the rebase, re-run the targeted registration/provider-config tests and account for any drift caused by current main no longer emitting startup banner lines through console.error. Preserve the actual runtime-config fallback behavior being tested; only update the assertions to match the current info-log surface used by registration startup banners. --------- Co-authored-by: Josh Lehman <josh@martian.engineering>
1 parent 00d1fa2 commit 2d1446f

4 files changed

Lines changed: 147 additions & 8 deletions

File tree

.changeset/late-cups-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@martian-engineering/lossless-claw": patch
3+
---
4+
5+
Fix startup-time summary model resolution when OpenClaw populates plugin config before the top-level runtime config surface.

src/plugin/index.ts

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,72 @@ function readDefaultModelFromConfig(config: unknown): string {
344344
return typeof primary === "string" ? primary.trim() : "";
345345
}
346346

347+
/** Load the best available validated OpenClaw config during plugin registration. */
348+
function loadEffectiveOpenClawConfig(api: OpenClawPluginApi): unknown {
349+
try {
350+
const runtimeConfig = api.runtime.config.loadConfig();
351+
if (runtimeConfig !== undefined) {
352+
if (isRecord(runtimeConfig) && Object.keys(runtimeConfig).length > 0) {
353+
return runtimeConfig;
354+
}
355+
if (!isRecord(api.config) || Object.keys(api.config).length === 0) {
356+
return runtimeConfig;
357+
}
358+
}
359+
} catch {
360+
// Older runtimes or early startup can leave loadConfig unavailable.
361+
}
362+
return api.config;
363+
}
364+
365+
/** Read this plugin's config from the validated OpenClaw runtime config. */
366+
function readPluginConfigFromOpenClawConfig(
367+
openClawConfig: unknown,
368+
pluginId: string,
369+
): Record<string, unknown> | undefined {
370+
if (!isRecord(openClawConfig)) {
371+
return undefined;
372+
}
373+
374+
const plugins = openClawConfig.plugins;
375+
if (!isRecord(plugins)) {
376+
return undefined;
377+
}
378+
379+
const entries = plugins.entries;
380+
if (!isRecord(entries)) {
381+
return undefined;
382+
}
383+
384+
const entry = entries[pluginId];
385+
if (!isRecord(entry) || !isRecord(entry.config)) {
386+
return undefined;
387+
}
388+
389+
return entry.config;
390+
}
391+
392+
/** Resolve the config surfaces that should drive registration-time behavior. */
393+
function resolveRegistrationConfig(api: OpenClawPluginApi): {
394+
openClawConfig: unknown;
395+
pluginConfig?: Record<string, unknown>;
396+
} {
397+
const openClawConfig = loadEffectiveOpenClawConfig(api);
398+
const apiPluginConfig =
399+
api.pluginConfig && typeof api.pluginConfig === "object" && !Array.isArray(api.pluginConfig)
400+
? api.pluginConfig
401+
: undefined;
402+
403+
if (apiPluginConfig && Object.keys(apiPluginConfig).length > 0) {
404+
return { openClawConfig, pluginConfig: apiPluginConfig };
405+
}
406+
407+
return {
408+
openClawConfig,
409+
pluginConfig: readPluginConfigFromOpenClawConfig(openClawConfig, api.id),
410+
};
411+
}
412+
347413
/** Read OpenClaw's configured compaction model from the validated runtime config. */
348414
function readCompactionModelFromConfig(config: unknown): string {
349415
if (!config || typeof config !== "object") {
@@ -1278,12 +1344,15 @@ function readLatestAssistantReply(messages: unknown[]): string | undefined {
12781344
}
12791345

12801346
/** Construct LCM dependencies from plugin API/runtime surfaces. */
1281-
function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
1347+
function createLcmDependencies(
1348+
api: OpenClawPluginApi,
1349+
registrationConfig = resolveRegistrationConfig(api),
1350+
): LcmDependencies {
12821351
const envSnapshot = snapshotPluginEnv();
1283-
envSnapshot.openclawDefaultModel = readDefaultModelFromConfig(api.config);
1352+
envSnapshot.openclawDefaultModel = readDefaultModelFromConfig(registrationConfig.openClawConfig);
12841353
const modelAuth = getRuntimeModelAuth(api);
12851354
const readEnv: ReadEnvFn = (key) => process.env[key];
1286-
const pluginConfig = resolvePluginConfig(api);
1355+
const pluginConfig = registrationConfig.pluginConfig;
12871356
const log = createLcmLogger(api);
12881357
const { config, diagnostics } = resolveLcmConfigWithDiagnostics(process.env, pluginConfig);
12891358

@@ -1943,7 +2012,8 @@ const lcmPlugin = {
19432012
},
19442013

19452014
register(api: OpenClawPluginApi) {
1946-
const deps = createLcmDependencies(api);
2015+
const registrationConfig = resolveRegistrationConfig(api);
2016+
const deps = createLcmDependencies(api, registrationConfig);
19472017
const dbPath = deps.config.databasePath;
19482018
const normalizedDbPath = normalizePath(dbPath);
19492019

@@ -2132,7 +2202,7 @@ const lcmPlugin = {
21322202
log: (message) => deps.log.info(message),
21332203
message: buildCompactionModelLog({
21342204
config: deps.config,
2135-
openClawConfig: api.config,
2205+
openClawConfig: registrationConfig.openClawConfig,
21362206
defaultProvider: process.env.OPENCLAW_PROVIDER?.trim() ?? "",
21372207
}),
21382208
});

test/index-complete-provider-config.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ describe("createLcmDependencies.complete provider config resolution", () => {
157157
model: "unit-model",
158158
});
159159

160-
expect(loadConfig).toHaveBeenCalledTimes(1);
160+
expect(loadConfig).toHaveBeenCalled();
161161
expect(piAiMock.completeSimple).toHaveBeenCalledTimes(1);
162162
expect(piAiMock.completeSimple).toHaveBeenCalledWith(
163163
expect.objectContaining({

test/plugin-config-registration.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ type HookHandler = (event: unknown, context: unknown) => unknown;
1414

1515
function buildApi(
1616
pluginConfig: unknown,
17-
options?: { includeModelAuth?: boolean; agentDir?: string },
17+
options?: {
18+
includeModelAuth?: boolean;
19+
agentDir?: string;
20+
runtimeConfig?: Record<string, unknown>;
21+
},
1822
): {
1923
api: OpenClawPluginApi;
2024
getFactory: () => RegisteredEngineFactory;
@@ -58,7 +62,7 @@ function buildApi(
5862
},
5963
}),
6064
config: {
61-
loadConfig: vi.fn(() => ({})),
65+
loadConfig: vi.fn(() => options?.runtimeConfig ?? {}),
6266
},
6367
logging: {
6468
getChildLogger: vi.fn(() => ({
@@ -519,6 +523,66 @@ describe("lcm plugin registration", () => {
519523
expect(sessionInfoLog).not.toHaveBeenCalled();
520524
});
521525

526+
it("falls back to runtime plugin config for the startup banner when register runs before api.pluginConfig is populated", () => {
527+
const { api, infoLog } = buildApi(
528+
{},
529+
{
530+
runtimeConfig: {
531+
plugins: {
532+
entries: {
533+
"lossless-claw": {
534+
enabled: true,
535+
config: {
536+
summaryModel: "openai-codex/gpt-5.4",
537+
},
538+
},
539+
},
540+
},
541+
},
542+
},
543+
);
544+
api.config = {} as OpenClawPluginApi["config"];
545+
546+
lcmPlugin.register(api);
547+
548+
expect(infoLog).toHaveBeenCalledWith(
549+
"[lcm] Compaction summarization model: openai-codex/gpt-5.4 (override)",
550+
);
551+
});
552+
553+
it("uses runtime OpenClaw defaults when api.pluginConfig is ready before api.config", () => {
554+
const { api, getFactory, infoLog } = buildApi(
555+
{
556+
enabled: true,
557+
},
558+
{
559+
runtimeConfig: compactionAndDefaultModelConfig({
560+
defaultModel: "anthropic/claude-sonnet-4-6",
561+
}),
562+
},
563+
);
564+
api.config = {} as OpenClawPluginApi["config"];
565+
566+
lcmPlugin.register(api);
567+
568+
expect(infoLog).toHaveBeenCalledWith(
569+
"[lcm] Compaction summarization model: anthropic/claude-sonnet-4-6 (default)",
570+
);
571+
572+
const factory = getFactory();
573+
expect(factory).toBeTypeOf("function");
574+
575+
const engine = factory!() as { deps?: { resolveModel: (modelRef?: string, providerHint?: string) => unknown } };
576+
const resolved = engine.deps?.resolveModel(undefined, undefined) as
577+
| { provider: string; model: string }
578+
| undefined;
579+
580+
expect(resolved).toEqual({
581+
provider: "anthropic",
582+
model: "claude-sonnet-4-6",
583+
});
584+
});
585+
522586
it("logs the OpenClaw compaction model at startup when no plugin override is set", () => {
523587
const { api, infoLog } = buildApi({
524588
enabled: true,

0 commit comments

Comments
 (0)