fix(sessions): harden session id resolution

This commit is contained in:
Peter Steinberger
2026-04-03 20:28:52 +09:00
parent 1337be3063
commit 1dd88c6288
5 changed files with 207 additions and 9 deletions

View File

@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions/types.js";
const hoisted = vi.hoisted(() => ({
loadSessionStoreMock: vi.fn<(storePath: string) => Record<string, SessionEntry>>(),
listAgentIdsMock: vi.fn<() => string[]>(),
}));
vi.mock("../../config/sessions.js", async () => {
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
"../../config/sessions.js",
);
return {
...actual,
loadSessionStore: (storePath: string) => hoisted.loadSessionStoreMock(storePath),
resolveStorePath: (store?: string, params?: { agentId?: string }) =>
`/stores/${params?.agentId ?? "main"}.json`,
resolveAgentIdFromSessionKey: () => "main",
resolveExplicitAgentSessionKey: () => undefined,
};
});
vi.mock("../agent-scope.js", () => ({
listAgentIds: () => hoisted.listAgentIdsMock(),
}));
const { resolveSessionKeyForRequest } = await import("./session.js");
describe("resolveSessionKeyForRequest", () => {
beforeEach(() => {
hoisted.loadSessionStoreMock.mockReset();
hoisted.listAgentIdsMock.mockReset();
hoisted.listAgentIdsMock.mockReturnValue(["main", "other"]);
});
it("prefers the current store when equal duplicates exist across stores", () => {
const mainStore = {
"agent:main:main": { sessionId: "sid", updatedAt: 10 },
} satisfies Record<string, SessionEntry>;
const otherStore = {
"agent:other:main": { sessionId: "sid", updatedAt: 10 },
} satisfies Record<string, SessionEntry>;
hoisted.loadSessionStoreMock.mockImplementation((storePath) => {
if (storePath === "/stores/main.json") {
return mainStore;
}
if (storePath === "/stores/other.json") {
return otherStore;
}
return {};
});
const result = resolveSessionKeyForRequest({
cfg: {
session: {
store: "/stores/{agentId}.json",
},
} satisfies OpenClawConfig,
sessionId: "sid",
});
expect(result.sessionKey).toBe("agent:main:main");
expect(result.sessionStore).toBe(mainStore);
expect(result.storePath).toBe("/stores/main.json");
});
});

View File

@@ -76,16 +76,21 @@ export function resolveSessionKeyForRequest(opts: {
(!sessionKey || sessionStore[sessionKey]?.sessionId !== opts.sessionId)
) {
const matches: Array<[string, SessionEntry]> = [];
const primaryStoreMatches: Array<[string, SessionEntry]> = [];
const storeByKey = new Map<string, SessionKeyResolution>();
const addMatches = (
candidateStore: Record<string, SessionEntry>,
candidateStorePath: string,
options?: { primary?: boolean },
): void => {
for (const [candidateKey, candidateEntry] of Object.entries(candidateStore)) {
if (candidateEntry?.sessionId !== opts.sessionId) {
continue;
}
matches.push([candidateKey, candidateEntry]);
if (options?.primary) {
primaryStoreMatches.push([candidateKey, candidateEntry]);
}
storeByKey.set(candidateKey, {
sessionKey: candidateKey,
sessionStore: candidateStore,
@@ -94,7 +99,7 @@ export function resolveSessionKeyForRequest(opts: {
}
};
addMatches(sessionStore, storePath);
addMatches(sessionStore, storePath, { primary: true });
const allAgentIds = listAgentIds(opts.cfg);
for (const agentId of allAgentIds) {
if (agentId === storeAgentId) {
@@ -106,12 +111,15 @@ export function resolveSessionKeyForRequest(opts: {
}
const preferredKey = resolvePreferredSessionKeyForSessionIdMatches(matches, opts.sessionId);
if (preferredKey) {
const preferred = storeByKey.get(preferredKey);
const currentStorePreferredKey =
preferredKey ??
resolvePreferredSessionKeyForSessionIdMatches(primaryStoreMatches, opts.sessionId);
if (currentStorePreferredKey) {
const preferred = storeByKey.get(currentStorePreferredKey);
if (preferred) {
return preferred;
}
sessionKey = preferredKey;
sessionKey = currentStorePreferredKey;
}
}