mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
fix: prefer freshest duplicate store matches
This commit is contained in:
@@ -375,6 +375,57 @@ describe("gateway session utils", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("loadSessionEntry prefers the freshest duplicate row across discovered stores", async () => {
|
||||||
|
clearConfigCache();
|
||||||
|
try {
|
||||||
|
await withStateDirEnv("session-utils-load-entry-cross-store-", async ({ stateDir }) => {
|
||||||
|
const canonicalSessionsDir = path.join(stateDir, "agents", "main", "sessions");
|
||||||
|
fs.mkdirSync(canonicalSessionsDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(canonicalSessionsDir, "sessions.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
"agent:main:main": { sessionId: "sess-canonical-stale", updatedAt: 10 },
|
||||||
|
"agent:main:MAIN": { sessionId: "sess-canonical-fresh", updatedAt: 1000 },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const discoveredSessionsDir = path.join(stateDir, "agents", "main ", "sessions");
|
||||||
|
fs.mkdirSync(discoveredSessionsDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(discoveredSessionsDir, "sessions.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
"agent:main:main": { sessionId: "sess-discovered-mid", updatedAt: 500 },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeConfigFile({
|
||||||
|
session: {
|
||||||
|
mainKey: "main",
|
||||||
|
store: path.join(stateDir, "agents", "{agentId}", "sessions", "sessions.json"),
|
||||||
|
},
|
||||||
|
agents: { list: [{ id: "main", default: true }] },
|
||||||
|
});
|
||||||
|
clearConfigCache();
|
||||||
|
|
||||||
|
const loaded = loadSessionEntry("agent:main:main");
|
||||||
|
|
||||||
|
expect(loaded.entry?.sessionId).toBe("sess-canonical-fresh");
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearConfigCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("pruneLegacyStoreKeys removes alias and case-variant ghost keys", () => {
|
test("pruneLegacyStoreKeys removes alias and case-variant ghost keys", () => {
|
||||||
const store: Record<string, unknown> = {
|
const store: Record<string, unknown> = {
|
||||||
"agent:ops:work": { sessionId: "canonical", updatedAt: 3 },
|
"agent:ops:work": { sessionId: "canonical", updatedAt: 3 },
|
||||||
|
|||||||
@@ -399,29 +399,33 @@ export function resolveFreshestSessionEntryFromStoreKeys(
|
|||||||
return resolveFreshestSessionStoreMatchFromStoreKeys(store, storeKeys)?.entry;
|
return resolveFreshestSessionStoreMatchFromStoreKeys(store, storeKeys)?.entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function findFreshestStoreMatch(
|
||||||
* Find a session entry by exact or case-insensitive key match.
|
|
||||||
* Returns both the entry and the actual store key it was found under,
|
|
||||||
* so callers can clean up legacy mixed-case keys when they differ from canonicalKey.
|
|
||||||
*/
|
|
||||||
function findStoreMatch(
|
|
||||||
store: Record<string, SessionEntry>,
|
store: Record<string, SessionEntry>,
|
||||||
...candidates: string[]
|
...candidates: string[]
|
||||||
): { entry: SessionEntry; key: string } | undefined {
|
): { entry: SessionEntry; key: string } | undefined {
|
||||||
// Exact match first.
|
const matches = new Map<string, { entry: SessionEntry; key: string }>();
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (candidate && store[candidate]) {
|
const trimmed = candidate.trim();
|
||||||
return { entry: store[candidate], key: candidate };
|
if (!trimmed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const exact = store[trimmed];
|
||||||
|
if (exact) {
|
||||||
|
matches.set(trimmed, { entry: exact, key: trimmed });
|
||||||
|
}
|
||||||
|
for (const key of findStoreKeysIgnoreCase(store, trimmed)) {
|
||||||
|
const entry = store[key];
|
||||||
|
if (entry) {
|
||||||
|
matches.set(key, { entry, key });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Case-insensitive scan for ALL candidates.
|
if (matches.size === 0) {
|
||||||
const loweredSet = new Set(candidates.filter(Boolean).map((c) => c.toLowerCase()));
|
return undefined;
|
||||||
for (const key of Object.keys(store)) {
|
|
||||||
if (loweredSet.has(key.toLowerCase())) {
|
|
||||||
return { entry: store[key], key };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return [...matches.values()].toSorted(
|
||||||
|
(a, b) => (b.entry.updatedAt ?? 0) - (a.entry.updatedAt ?? 0),
|
||||||
|
)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -782,7 +786,7 @@ function resolveGatewaySessionStoreLookup(params: {
|
|||||||
};
|
};
|
||||||
let selectedStorePath = fallback.storePath;
|
let selectedStorePath = fallback.storePath;
|
||||||
let selectedStore = params.initialStore ?? loadSessionStore(fallback.storePath);
|
let selectedStore = params.initialStore ?? loadSessionStore(fallback.storePath);
|
||||||
let selectedMatch = findStoreMatch(selectedStore, ...scanTargets);
|
let selectedMatch = findFreshestStoreMatch(selectedStore, ...scanTargets);
|
||||||
let selectedUpdatedAt = selectedMatch?.entry.updatedAt ?? Number.NEGATIVE_INFINITY;
|
let selectedUpdatedAt = selectedMatch?.entry.updatedAt ?? Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
for (let index = 1; index < candidates.length; index += 1) {
|
for (let index = 1; index < candidates.length; index += 1) {
|
||||||
@@ -791,7 +795,7 @@ function resolveGatewaySessionStoreLookup(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const store = loadSessionStore(candidate.storePath);
|
const store = loadSessionStore(candidate.storePath);
|
||||||
const match = findStoreMatch(store, ...scanTargets);
|
const match = findFreshestStoreMatch(store, ...scanTargets);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user