mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 15:41:40 +00:00
fix: prefer freshest duplicate session rows in reads
This commit is contained in:
@@ -55,6 +55,7 @@ import {
|
||||
loadSessionEntry,
|
||||
migrateAndPruneGatewaySessionStoreKey,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
resolveFreshestSessionEntryFromStoreKeys,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionModelRef,
|
||||
resolveSessionTranscriptCandidates,
|
||||
@@ -583,7 +584,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
key,
|
||||
store,
|
||||
});
|
||||
const entry = target.storeKeys.map((candidate) => store[candidate]).find(Boolean);
|
||||
const entry = resolveFreshestSessionEntryFromStoreKeys(store, target.storeKeys);
|
||||
if (!entry?.sessionId) {
|
||||
previews.push({ key, status: "missing", items: [] });
|
||||
continue;
|
||||
@@ -1027,7 +1028,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
|
||||
const { target, storePath } = resolveGatewaySessionTargetFromKey(key);
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = target.storeKeys.map((k) => store[k]).find(Boolean);
|
||||
const entry = resolveFreshestSessionEntryFromStoreKeys(store, target.storeKeys);
|
||||
if (!entry?.sessionId) {
|
||||
respond(true, { messages: [] }, undefined);
|
||||
return;
|
||||
|
||||
@@ -1014,6 +1014,55 @@ describe("gateway server sessions", () => {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("sessions.preview prefers the freshest duplicate row for a legacy mixed-case main alias", async () => {
|
||||
const { dir, storePath } = await createSessionStoreDir();
|
||||
testState.agentsConfig = { list: [{ id: "ops", default: true }] };
|
||||
testState.sessionConfig = { mainKey: "work" };
|
||||
|
||||
const staleTranscriptPath = path.join(dir, "sess-stale-main.jsonl");
|
||||
const freshTranscriptPath = path.join(dir, "sess-fresh-main.jsonl");
|
||||
await fs.writeFile(
|
||||
staleTranscriptPath,
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: "sess-stale-main" }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "stale preview" } }),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
freshTranscriptPath,
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: "sess-fresh-main" }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "fresh preview" } }),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:ops:work": {
|
||||
sessionId: "sess-stale-main",
|
||||
updatedAt: 1,
|
||||
},
|
||||
"agent:ops:WORK": {
|
||||
sessionId: "sess-fresh-main",
|
||||
updatedAt: 2,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { ws } = await openClient();
|
||||
const entry = await getMainPreviewEntry(ws);
|
||||
expect(entry?.items[0]?.text).toContain("fresh preview");
|
||||
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("sessions.resolve and mutators clean legacy main-alias ghost keys", async () => {
|
||||
const { dir, storePath } = await createSessionStoreDir();
|
||||
testState.agentsConfig = { list: [{ id: "ops", default: true }] };
|
||||
|
||||
@@ -367,6 +367,22 @@ export function loadSessionEntry(sessionKey: string) {
|
||||
return { cfg, storePath, store, entry: match?.entry, canonicalKey, legacyKey };
|
||||
}
|
||||
|
||||
export function resolveFreshestSessionEntryFromStoreKeys(
|
||||
store: Record<string, SessionEntry>,
|
||||
storeKeys: string[],
|
||||
): SessionEntry | undefined {
|
||||
const matches = storeKeys
|
||||
.map((key) => store[key])
|
||||
.filter((entry): entry is SessionEntry => Boolean(entry));
|
||||
if (matches.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (matches.length === 1) {
|
||||
return matches[0];
|
||||
}
|
||||
return [...matches].toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a session entry by exact or case-insensitive key match.
|
||||
* Returns both the entry and the actual store key it was found under,
|
||||
|
||||
@@ -144,6 +144,73 @@ describe("session history HTTP endpoints", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("prefers the freshest duplicate row for direct history reads", async () => {
|
||||
const storePath = await createSessionStoreFile();
|
||||
const dir = path.dirname(storePath);
|
||||
const staleTranscriptPath = path.join(dir, "sess-stale-main.jsonl");
|
||||
const freshTranscriptPath = path.join(dir, "sess-fresh-main.jsonl");
|
||||
await fs.writeFile(
|
||||
staleTranscriptPath,
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: "sess-stale-main" }),
|
||||
JSON.stringify({
|
||||
message: { role: "assistant", content: [{ type: "text", text: "stale history" }] },
|
||||
}),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
freshTranscriptPath,
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: "sess-fresh-main" }),
|
||||
JSON.stringify({
|
||||
message: { role: "assistant", content: [{ type: "text", text: "fresh history" }] },
|
||||
}),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-stale-main",
|
||||
sessionFile: staleTranscriptPath,
|
||||
updatedAt: 1,
|
||||
},
|
||||
"agent:main:MAIN": {
|
||||
sessionId: "sess-fresh-main",
|
||||
sessionFile: freshTranscriptPath,
|
||||
updatedAt: 2,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const harness = await createGatewaySuiteHarness();
|
||||
try {
|
||||
const res = await fetch(
|
||||
`http://127.0.0.1:${harness.port}/sessions/${encodeURIComponent("agent:main:main")}/history`,
|
||||
{
|
||||
headers: AUTH_HEADER,
|
||||
},
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
sessionKey?: string;
|
||||
messages?: Array<{ content?: Array<{ text?: string }> }>;
|
||||
};
|
||||
expect(body.sessionKey).toBe("agent:main:main");
|
||||
expect(body.messages?.[0]?.content?.[0]?.text).toBe("fresh history");
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("supports cursor pagination over direct REST while preserving the messages field", async () => {
|
||||
const { storePath } = await seedSession({ text: "first message" });
|
||||
const second = await appendAssistantMessageToSessionTranscript({
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getHeader } from "./http-utils.js";
|
||||
import {
|
||||
attachOpenClawTranscriptMeta,
|
||||
readSessionMessages,
|
||||
resolveFreshestSessionEntryFromStoreKeys,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionTranscriptCandidates,
|
||||
} from "./session-utils.js";
|
||||
@@ -168,7 +169,7 @@ export async function handleSessionHistoryHttpRequest(
|
||||
|
||||
const target = resolveGatewaySessionStoreTarget({ cfg, key: sessionKey });
|
||||
const store = loadSessionStore(target.storePath);
|
||||
const entry = target.storeKeys.map((key) => store[key]).find(Boolean);
|
||||
const entry = resolveFreshestSessionEntryFromStoreKeys(store, target.storeKeys);
|
||||
if (!entry?.sessionId) {
|
||||
sendJson(res, 404, {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user