mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-24 08:02:23 +00:00
refactor: route live model reads through session accessor (#96206)
This commit is contained in:
@@ -82,9 +82,10 @@ export const migratedSessionAccessorFiles = new Set([
|
||||
"src/agents/embedded-agent-runner/tool-result-truncation.ts",
|
||||
"src/agents/embedded-agent-runner/transcript-rewrite.ts",
|
||||
"src/agents/embedded-agent-runner/transcript-runtime-state.ts",
|
||||
"src/auto-reply/reply/abort.ts",
|
||||
"src/agents/live-model-switch.ts",
|
||||
"src/agents/subagent-control.ts",
|
||||
"src/agents/subagent-registry-helpers.ts",
|
||||
"src/auto-reply/reply/abort.ts",
|
||||
"src/auto-reply/reply/agent-runner-helpers.ts",
|
||||
"src/auto-reply/reply/agent-runner.ts",
|
||||
"src/auto-reply/reply/commands-subagents/action-info.ts",
|
||||
|
||||
@@ -27,11 +27,6 @@ vi.mock("./model-selection.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../config/sessions/store.js", () => ({
|
||||
loadSessionStore: (...args: unknown[]) => state.loadSessionStoreMock(...args),
|
||||
updateSessionStore: (...args: unknown[]) => state.updateSessionStoreMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../config/sessions/session-accessor.js", () => ({
|
||||
loadSessionEntry: (scope: { sessionKey: string }) => {
|
||||
const store = state.loadSessionStoreMock(scope) as Record<string, unknown> | undefined;
|
||||
@@ -44,12 +39,6 @@ vi.mock("../config/sessions/paths.js", () => ({
|
||||
resolveStorePath: (...args: unknown[]) => state.resolveStorePathMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../config/sessions.js", () => ({
|
||||
loadSessionStore: (...args: unknown[]) => state.loadSessionStoreMock(...args),
|
||||
resolveStorePath: (...args: unknown[]) => state.resolveStorePathMock(...args),
|
||||
updateSessionStore: (...args: unknown[]) => state.updateSessionStoreMock(...args),
|
||||
}));
|
||||
|
||||
let mod: typeof import("./live-model-switch.js");
|
||||
|
||||
async function loadModule() {
|
||||
@@ -193,6 +182,12 @@ describe("live model switch", () => {
|
||||
expect(state.resolveStorePathMock).toHaveBeenCalledWith("/tmp/custom-store.json", {
|
||||
agentId: "reply",
|
||||
});
|
||||
expect(state.loadSessionStoreMock).toHaveBeenCalledWith({
|
||||
storePath: "/tmp/session-store.json",
|
||||
sessionKey: "main",
|
||||
hydrateSkillPromptRefs: false,
|
||||
readConsistency: "latest",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers persisted session overrides ahead of stale runtime model fields", async () => {
|
||||
@@ -468,10 +463,12 @@ describe("live model switch", () => {
|
||||
const result = shouldSwitchToLiveModel(makeShouldSwitchParams());
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(state.loadSessionStoreMock).toHaveBeenCalledWith("/tmp/session-store.json", {
|
||||
expect(state.loadSessionStoreMock).toHaveBeenCalledWith({
|
||||
hydrateSkillPromptRefs: false,
|
||||
skipCache: true,
|
||||
clone: false,
|
||||
readConsistency: "latest",
|
||||
sessionKey: "main",
|
||||
storePath: "/tmp/session-store.json",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
*/
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
import { resolveStorePath } from "../config/sessions/paths.js";
|
||||
import { patchSessionEntry } from "../config/sessions/session-accessor.js";
|
||||
import { loadSessionStore } from "../config/sessions/store.js";
|
||||
import { loadSessionEntry, patchSessionEntry } from "../config/sessions/session-accessor.js";
|
||||
import {
|
||||
normalizeStoredOverrideModel,
|
||||
resolveDefaultModelForAgent,
|
||||
@@ -45,10 +43,12 @@ export function resolveLiveSessionModelSelection(params: {
|
||||
const storePath = resolveStorePath(cfg.session?.store, {
|
||||
agentId,
|
||||
});
|
||||
const entry = loadSessionStore(storePath, {
|
||||
const entry = loadSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
hydrateSkillPromptRefs: false,
|
||||
skipCache: true,
|
||||
})[sessionKey];
|
||||
readConsistency: "latest",
|
||||
});
|
||||
const normalizedSelection = normalizeStoredOverrideModel({
|
||||
providerOverride: entry?.providerOverride,
|
||||
modelOverride: entry?.modelOverride,
|
||||
@@ -151,11 +151,13 @@ export function shouldSwitchToLiveModel(params: {
|
||||
const storePath = resolveStorePath(cfg.session?.store, {
|
||||
agentId: params.agentId?.trim(),
|
||||
});
|
||||
const entry = loadSessionStore(storePath, {
|
||||
const entry = loadSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
hydrateSkillPromptRefs: false,
|
||||
skipCache: true,
|
||||
clone: false,
|
||||
})[sessionKey];
|
||||
readConsistency: "latest",
|
||||
});
|
||||
if (!entry?.liveModelSwitchPending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SessionManager } from "../../agents/sessions/session-manager.js";
|
||||
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
import type { OpenClawConfig } from "../types.openclaw.js";
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
updateSessionEntry,
|
||||
upsertSessionEntry,
|
||||
} from "./session-accessor.js";
|
||||
import * as sessionStore from "./store.js";
|
||||
import { loadSessionStore, saveSessionStore, updateSessionStoreEntry } from "./store.js";
|
||||
import { withOwnedSessionTranscriptWrites } from "./transcript-write-context.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
@@ -464,6 +465,47 @@ describe("session accessor file-backed seam", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps latest entry reads to the file backend cache bypass", () => {
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify({
|
||||
"agent:main:main": {
|
||||
sessionId: "session-1",
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const loadSessionStoreSpy = vi.spyOn(sessionStore, "loadSessionStore");
|
||||
|
||||
try {
|
||||
expect(
|
||||
loadSessionEntry({
|
||||
readConsistency: "latest",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath,
|
||||
})?.model,
|
||||
).toBe("gpt-5.4");
|
||||
expect(loadSessionStoreSpy).toHaveBeenLastCalledWith(
|
||||
storePath,
|
||||
expect.objectContaining({ skipCache: true }),
|
||||
);
|
||||
|
||||
loadSessionEntry({
|
||||
clone: false,
|
||||
readConsistency: "latest",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath,
|
||||
});
|
||||
expect(loadSessionStoreSpy).toHaveBeenLastCalledWith(
|
||||
storePath,
|
||||
expect.objectContaining({ clone: false, skipCache: true }),
|
||||
);
|
||||
} finally {
|
||||
loadSessionStoreSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves canonical entry reads without requiring exact key casing", async () => {
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
|
||||
@@ -125,6 +125,8 @@ export type SessionAccessScope = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
/** Set false for metadata-only reads that do not need hydrated prompt refs. */
|
||||
hydrateSkillPromptRefs?: boolean;
|
||||
/** Use latest when the caller must bypass any in-process metadata snapshot. */
|
||||
readConsistency?: "latest";
|
||||
/** Canonical or alias session key for the entry being read or written. */
|
||||
sessionKey: string;
|
||||
/** Explicit store path for callers that already resolved the owning store. */
|
||||
@@ -720,9 +722,10 @@ export async function updateResolvedSessionEntry<T>(
|
||||
|
||||
/** Returns the entry for a canonical or alias session key, if one exists. */
|
||||
export function loadSessionEntry(scope: SessionAccessScope): SessionEntry | undefined {
|
||||
if (scope.clone === false) {
|
||||
if (scope.clone === false || scope.readConsistency === "latest") {
|
||||
const store = loadSessionStore(resolveAccessStorePath(scope), {
|
||||
clone: false,
|
||||
...(scope.clone === false ? { clone: false } : {}),
|
||||
...(scope.readConsistency === "latest" ? { skipCache: true } : {}),
|
||||
...(scope.hydrateSkillPromptRefs === false ? { hydrateSkillPromptRefs: false } : {}),
|
||||
});
|
||||
return resolveSessionStoreEntry({ store, sessionKey: scope.sessionKey }).existing;
|
||||
|
||||
@@ -32,9 +32,10 @@ describe("session accessor boundary guard", () => {
|
||||
"src/agents/embedded-agent-runner/tool-result-truncation.ts",
|
||||
"src/agents/embedded-agent-runner/transcript-rewrite.ts",
|
||||
"src/agents/embedded-agent-runner/transcript-runtime-state.ts",
|
||||
"src/auto-reply/reply/abort.ts",
|
||||
"src/agents/live-model-switch.ts",
|
||||
"src/agents/subagent-control.ts",
|
||||
"src/agents/subagent-registry-helpers.ts",
|
||||
"src/auto-reply/reply/abort.ts",
|
||||
"src/auto-reply/reply/agent-runner-helpers.ts",
|
||||
"src/auto-reply/reply/agent-runner.ts",
|
||||
"src/auto-reply/reply/commands-subagents/action-info.ts",
|
||||
|
||||
Reference in New Issue
Block a user