mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:08:28 +00:00
fix(sessions): resolve transcript paths with explicit agent context (#16288)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 7cbe9deca9
Co-authored-by: robbyczgw-cla <239660374+robbyczgw-cla@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -53,8 +53,9 @@ function resolveTranscriptPath(params: {
|
||||
sessionId: string;
|
||||
storePath: string | undefined;
|
||||
sessionFile?: string;
|
||||
agentId?: string;
|
||||
}): string | null {
|
||||
const { sessionId, storePath, sessionFile } = params;
|
||||
const { sessionId, storePath, sessionFile, agentId } = params;
|
||||
if (!storePath && !sessionFile) {
|
||||
return null;
|
||||
}
|
||||
@@ -63,7 +64,7 @@ function resolveTranscriptPath(params: {
|
||||
return resolveSessionFilePath(
|
||||
sessionId,
|
||||
sessionFile ? { sessionFile } : undefined,
|
||||
sessionsDir ? { sessionsDir } : undefined,
|
||||
sessionsDir || agentId ? { sessionsDir, agentId } : undefined,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
@@ -99,12 +100,14 @@ function appendAssistantTranscriptMessage(params: {
|
||||
sessionId: string;
|
||||
storePath: string | undefined;
|
||||
sessionFile?: string;
|
||||
agentId?: string;
|
||||
createIfMissing?: boolean;
|
||||
}): TranscriptAppendResult {
|
||||
const transcriptPath = resolveTranscriptPath({
|
||||
sessionId: params.sessionId,
|
||||
storePath: params.storePath,
|
||||
sessionFile: params.sessionFile,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
if (!transcriptPath) {
|
||||
return { ok: false, error: "transcript path not resolved" };
|
||||
@@ -572,6 +575,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
sessionId,
|
||||
storePath: latestStorePath,
|
||||
sessionFile: latestEntry?.sessionFile,
|
||||
agentId,
|
||||
createIfMissing: true,
|
||||
});
|
||||
if (appended.ok) {
|
||||
@@ -666,7 +670,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
|
||||
// Load session to find transcript file
|
||||
const rawSessionKey = p.sessionKey;
|
||||
const { storePath, entry } = loadSessionEntry(rawSessionKey);
|
||||
const { cfg, storePath, entry } = loadSessionEntry(rawSessionKey);
|
||||
const sessionId = entry?.sessionId;
|
||||
if (!sessionId || !storePath) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "session not found"));
|
||||
@@ -679,6 +683,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
sessionId,
|
||||
storePath,
|
||||
sessionFile: entry?.sessionFile,
|
||||
agentId: resolveSessionAgentId({ sessionKey: rawSessionKey, config: cfg }),
|
||||
createIfMissing: false,
|
||||
});
|
||||
if (!appended.ok || !appended.messageId || !appended.message) {
|
||||
|
||||
@@ -475,6 +475,58 @@ describe("readSessionMessages", () => {
|
||||
expect(marker.__openclaw?.id).toBe("comp-1");
|
||||
expect(typeof marker.timestamp).toBe("number");
|
||||
});
|
||||
|
||||
test("reads cross-agent absolute sessionFile when storePath points to another agent dir", () => {
|
||||
const sessionId = "cross-agent-default-root";
|
||||
const sessionFile = path.join(tmpDir, "agents", "ops", "sessions", `${sessionId}.jsonl`);
|
||||
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
sessionFile,
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({ message: { role: "user", content: "from-ops" } }),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const wrongStorePath = path.join(tmpDir, "agents", "main", "sessions", "sessions.json");
|
||||
const out = readSessionMessages(sessionId, wrongStorePath, sessionFile);
|
||||
|
||||
expect(out).toEqual([{ role: "user", content: "from-ops" }]);
|
||||
});
|
||||
|
||||
test("reads cross-agent absolute sessionFile for custom per-agent store roots", () => {
|
||||
const sessionId = "cross-agent-custom-root";
|
||||
const sessionFile = path.join(
|
||||
tmpDir,
|
||||
"custom",
|
||||
"agents",
|
||||
"ops",
|
||||
"sessions",
|
||||
`${sessionId}.jsonl`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
sessionFile,
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "from-custom-ops" } }),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const wrongStorePath = path.join(
|
||||
tmpDir,
|
||||
"custom",
|
||||
"agents",
|
||||
"main",
|
||||
"sessions",
|
||||
"sessions.json",
|
||||
);
|
||||
const out = readSessionMessages(sessionId, wrongStorePath, sessionFile);
|
||||
|
||||
expect(out).toEqual([{ role: "assistant", content: "from-custom-ops" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readSessionPreviewItemsFromTranscript", () => {
|
||||
@@ -594,6 +646,22 @@ describe("resolveSessionTranscriptCandidates", () => {
|
||||
});
|
||||
|
||||
describe("resolveSessionTranscriptCandidates safety", () => {
|
||||
test("keeps cross-agent absolute sessionFile when storePath agent context differs", () => {
|
||||
const storePath = "/tmp/openclaw/agents/main/sessions/sessions.json";
|
||||
const sessionFile = "/tmp/openclaw/agents/ops/sessions/sess-safe.jsonl";
|
||||
const candidates = resolveSessionTranscriptCandidates("sess-safe", storePath, sessionFile);
|
||||
|
||||
expect(candidates.map((value) => path.resolve(value))).toContain(path.resolve(sessionFile));
|
||||
});
|
||||
|
||||
test("keeps cross-agent absolute sessionFile for custom per-agent store roots", () => {
|
||||
const storePath = "/srv/custom/agents/main/sessions/sessions.json";
|
||||
const sessionFile = "/srv/custom/agents/ops/sessions/sess-safe.jsonl";
|
||||
const candidates = resolveSessionTranscriptCandidates("sess-safe", storePath, sessionFile);
|
||||
|
||||
expect(candidates.map((value) => path.resolve(value))).toContain(path.resolve(sessionFile));
|
||||
});
|
||||
|
||||
test("drops unsafe session IDs instead of producing traversal paths", () => {
|
||||
const candidates = resolveSessionTranscriptCandidates(
|
||||
"../etc/passwd",
|
||||
|
||||
@@ -131,7 +131,9 @@ export function resolveSessionTranscriptCandidates(
|
||||
if (storePath) {
|
||||
const sessionsDir = path.dirname(storePath);
|
||||
if (sessionFile) {
|
||||
pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { sessionsDir }));
|
||||
pushCandidate(() =>
|
||||
resolveSessionFilePath(sessionId, { sessionFile }, { sessionsDir, agentId }),
|
||||
);
|
||||
}
|
||||
pushCandidate(() => resolveSessionTranscriptPathInDir(sessionId, sessionsDir));
|
||||
} else if (sessionFile) {
|
||||
|
||||
@@ -70,6 +70,25 @@ describe("gateway session utils", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSessionStoreKey falls back to first list entry when no agent is marked default", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "main" },
|
||||
agents: { list: [{ id: "ops" }, { id: "review" }] },
|
||||
} as OpenClawConfig;
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "main" })).toBe("agent:ops:main");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "discord:group:123" })).toBe(
|
||||
"agent:ops:discord:group:123",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSessionStoreKey falls back to main when agents.list is missing", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
} as OpenClawConfig;
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "main" })).toBe("agent:main:work");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "thread-1" })).toBe("agent:main:thread-1");
|
||||
});
|
||||
|
||||
test("resolveSessionStoreKey normalizes session key casing", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "main" },
|
||||
|
||||
Reference in New Issue
Block a user