fix(sessions): fast-path qualified row model refs

This commit is contained in:
Forge
2026-05-05 17:12:25 -07:00
committed by Ayaan Zaidi
parent e59890eff0
commit 8bfec5b9ac
3 changed files with 158 additions and 4 deletions

View File

@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
- PR triage: mark external pull requests with `proof: supplied` when Barnacle finds structured real behavior proof, keep stale negative proof labels in sync across CRLF-edited PR bodies, and let ClawSweeper own the stronger `proof: sufficient` judgement.
- Sessions CLI: show the selected agent runtime in the `openclaw sessions` table so terminal output matches the runtime visibility already present in JSON/status surfaces. Thanks @vincentkoc.
- Talk/voice: unify realtime relay, transcription relay, managed-room handoff, Voice Call, Google Meet, VoiceClaw, and native clients around a shared Talk session controller and add the Gateway-managed `talk.session.*` RPC surface.

View File

@@ -1440,6 +1440,44 @@ describe("listSessionsFromStore selected model display", () => {
expect(result.sessions[0]?.modelProvider).toBe("anthropic");
expect(result.sessions[0]?.model).toBe("claude-opus-4-7");
});
test("uses qualified selected defaults for rows without runtime model metadata", () => {
const cfg = {
agents: {
defaults: { model: { primary: "openai/gpt-5.4" } },
list: [
{ id: "main", model: { primary: "anthropic/claude-sonnet-4-6" } },
{
id: "review",
model: { primary: "vercel-ai-gateway/anthropic/claude-haiku-4-5" },
},
],
},
} as OpenClawConfig;
const result = listSessionsFromStore({
cfg,
storePath: "/tmp/sessions.json",
store: {
"agent:main:main": {
sessionId: "sess-main",
updatedAt: 2,
} as SessionEntry,
"agent:review:review": {
sessionId: "sess-review",
updatedAt: 1,
} as SessionEntry,
},
opts: {},
});
expect(
result.sessions.map((session) => [session.key, session.modelProvider, session.model]),
).toEqual([
["agent:main:main", "anthropic", "claude-sonnet-4-6"],
["agent:review:review", "vercel-ai-gateway", "anthropic/claude-haiku-4-5"],
]);
});
});
describe("resolveSessionModelIdentityRef", () => {

View File

@@ -19,6 +19,7 @@ import {
import {
inferUniqueProviderFromConfiguredModels,
isCliProvider,
normalizeProviderId,
normalizeStoredOverrideModel,
parseModelRef,
resolveConfiguredModelRef,
@@ -77,6 +78,7 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
normalizeOptionalLowercaseString,
resolvePrimaryStringValue,
} from "../shared/string-coerce.js";
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.shared.js";
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
@@ -373,6 +375,7 @@ type SessionListRowContext = {
subagentRuns: ReturnType<typeof buildSubagentRunReadIndex>;
storeChildSessionsByKey: Map<string, string[]>;
selectedModelByOverrideRef: Map<string, ReturnType<typeof resolveSessionModelRef>>;
modelIdentityByEntryKey: Map<string, ReturnType<typeof resolveSessionModelIdentityRef>>;
thinkingLevelsByModelRef: Map<string, ReturnType<typeof listThinkingLevelOptions>>;
};
@@ -491,6 +494,7 @@ function buildSessionListRowContext(params: {
subagentRuns,
storeChildSessionsByKey: buildStoreChildSessionIndex(params.store, params.now, subagentRuns),
selectedModelByOverrideRef: new Map(),
modelIdentityByEntryKey: new Map(),
thinkingLevelsByModelRef: new Map(),
};
}
@@ -499,6 +503,116 @@ function createSessionRowModelCacheKey(provider: string | undefined, model: stri
return `${normalizeLowercaseStringOrEmpty(provider)}\0${normalizeOptionalString(model) ?? ""}`;
}
function parseQualifiedModelRefForSessionList(
raw: string | undefined,
): { provider: string; model: string } | undefined {
const trimmed = normalizeOptionalString(raw);
const slash = trimmed?.indexOf("/") ?? -1;
if (!trimmed || slash <= 0 || slash === trimmed.length - 1) {
return undefined;
}
const provider = normalizeProviderId(trimmed.slice(0, slash).trim());
const model = normalizeOptionalString(trimmed.slice(slash + 1));
return model ? { provider, model } : undefined;
}
function createSessionDefaultModelCacheKey(cfg: OpenClawConfig, agentId?: string): string {
const normalizedAgentId = normalizeAgentId(agentId);
const primary = normalizedAgentId
? resolveAgentEffectiveModelPrimary(cfg, normalizedAgentId)
: resolvePrimaryStringValue(cfg.agents?.defaults?.model);
return normalizeOptionalString(primary) ?? "";
}
function createSessionEntryModelCacheKey(params: {
cfg: OpenClawConfig;
agentId?: string;
entry?:
| SessionEntry
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">;
fallbackModelRef?: string;
}) {
return [
createSessionDefaultModelCacheKey(params.cfg, params.agentId),
normalizeOptionalString(params.entry?.providerOverride) ?? "",
normalizeOptionalString(params.entry?.modelOverride) ?? "",
normalizeOptionalString(params.entry?.modelProvider) ?? "",
normalizeOptionalString(params.entry?.model) ?? "",
normalizeOptionalString(params.fallbackModelRef) ?? "",
].join("\0");
}
function resolveSessionDefaultModelRefForRow(
cfg: OpenClawConfig,
agentId?: string,
): { provider: string; model: string } {
if (agentId) {
const primary = resolveAgentEffectiveModelPrimary(cfg, agentId)?.trim();
const parsed = parseQualifiedModelRefForSessionList(primary);
if (parsed) {
return parsed;
}
return resolveDefaultModelForAgent({ cfg, agentId });
}
return resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
}
function resolveSessionRowModelIdentityRef(params: {
cfg: OpenClawConfig;
entry?:
| SessionEntry
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">;
agentId: string;
fallbackModelRef?: string;
rowContext?: SessionListRowContext;
}): ReturnType<typeof resolveSessionModelIdentityRef> {
if (!params.rowContext) {
return resolveSessionModelIdentityRef(
params.cfg,
params.entry,
params.agentId,
params.fallbackModelRef,
);
}
const key = createSessionEntryModelCacheKey({
cfg: params.cfg,
agentId: params.agentId,
entry: params.entry,
fallbackModelRef: params.fallbackModelRef,
});
const cached = params.rowContext.modelIdentityByEntryKey.get(key);
if (cached) {
return cached;
}
const runtimeModel = normalizeOptionalString(params.entry?.model);
const runtimeProvider = normalizeOptionalString(params.entry?.modelProvider);
if (
!runtimeModel &&
!runtimeProvider &&
!normalizeOptionalString(params.fallbackModelRef) &&
!normalizeOptionalString(params.entry?.providerOverride) &&
!normalizeOptionalString(params.entry?.modelOverride)
) {
const resolved = resolveSessionDefaultModelRefForRow(params.cfg, params.agentId);
params.rowContext.modelIdentityByEntryKey.set(key, resolved);
return resolved;
}
const resolved = resolveSessionModelIdentityRef(
params.cfg,
params.entry,
params.agentId,
params.fallbackModelRef,
);
params.rowContext.modelIdentityByEntryKey.set(key, resolved);
return resolved;
}
function resolveSessionSelectedModelRef(params: {
cfg: OpenClawConfig;
entry?: SessionEntry;
@@ -1578,12 +1692,13 @@ export function buildGatewaySessionRow(params: {
agentId: sessionAgentId,
rowContext,
});
const resolvedModel = resolveSessionModelIdentityRef(
const resolvedModel = resolveSessionRowModelIdentityRef({
cfg,
entry,
sessionAgentId,
subagentRun?.model,
);
agentId: sessionAgentId,
fallbackModelRef: subagentRun?.model,
rowContext,
});
const runtimeModelPresent =
Boolean(entry?.model?.trim()) || Boolean(entry?.modelProvider?.trim());
const needsTranscriptTotalTokens =