mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 01:11:16 +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", () => {
|
||||
const store: Record<string, unknown> = {
|
||||
"agent:ops:work": { sessionId: "canonical", updatedAt: 3 },
|
||||
|
||||
@@ -399,29 +399,33 @@ export function resolveFreshestSessionEntryFromStoreKeys(
|
||||
return resolveFreshestSessionStoreMatchFromStoreKeys(store, storeKeys)?.entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
function findFreshestStoreMatch(
|
||||
store: Record<string, SessionEntry>,
|
||||
...candidates: string[]
|
||||
): { entry: SessionEntry; key: string } | undefined {
|
||||
// Exact match first.
|
||||
const matches = new Map<string, { entry: SessionEntry; key: string }>();
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && store[candidate]) {
|
||||
return { entry: store[candidate], key: candidate };
|
||||
const trimmed = candidate.trim();
|
||||
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.
|
||||
const loweredSet = new Set(candidates.filter(Boolean).map((c) => c.toLowerCase()));
|
||||
for (const key of Object.keys(store)) {
|
||||
if (loweredSet.has(key.toLowerCase())) {
|
||||
return { entry: store[key], key };
|
||||
}
|
||||
if (matches.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
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 selectedStore = params.initialStore ?? loadSessionStore(fallback.storePath);
|
||||
let selectedMatch = findStoreMatch(selectedStore, ...scanTargets);
|
||||
let selectedMatch = findFreshestStoreMatch(selectedStore, ...scanTargets);
|
||||
let selectedUpdatedAt = selectedMatch?.entry.updatedAt ?? Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (let index = 1; index < candidates.length; index += 1) {
|
||||
@@ -791,7 +795,7 @@ function resolveGatewaySessionStoreLookup(params: {
|
||||
continue;
|
||||
}
|
||||
const store = loadSessionStore(candidate.storePath);
|
||||
const match = findStoreMatch(store, ...scanTargets);
|
||||
const match = findFreshestStoreMatch(store, ...scanTargets);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user