Sessions: fix sessions_list transcriptPath path resolution

This commit is contained in:
Vignesh Natarajan
2026-02-28 14:42:14 -08:00
parent f57b4669e1
commit c58d2aa99e
5 changed files with 59 additions and 7 deletions

View File

@@ -82,6 +82,7 @@ Docs: https://docs.openclaw.ai
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
- TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000.
- Memory/Hybrid recall: when strict hybrid scoring yields no hits, preserve keyword-backed matches using a text-weight floor so freshly indexed lexical canaries no longer disappear behind `minScore` filtering. (#29112) Thanks @ceo-nada.
- Agents/Sessions list transcript paths: resolve `sessions_list` `transcriptPath` via agent-aware session path options and ignore combined-store sentinel paths (`(multiple)`) so listed transcript paths always point to the state directory. (#28379) Thanks @fafuzuoluo.
- Podman/Quadlet setup: fix `sed` escaping and UID mismatch in Podman Quadlet setup. (#26414) Thanks @KnHack and @vincentkoc.
- Browser/Navigate: resolve the correct `targetId` in navigate responses after renderer swaps. (#25326) Thanks @stone-jin and @vincentkoc.
- Agents/Ollama discovery: skip Ollama discovery when explicit models are configured. (#28827) Thanks @Kansodata and @vincentkoc.

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
addSubagentRunForTests,
@@ -171,6 +172,46 @@ describe("sessions tools", () => {
expect(cronDetails.sessions?.[0]?.kind).toBe("cron");
});
it("sessions_list resolves transcriptPath from agent state dir for multi-store listings", async () => {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "sessions.list") {
return {
path: "(multiple)",
sessions: [
{
key: "main",
kind: "direct",
sessionId: "sess-main",
updatedAt: 12,
},
],
};
}
return {};
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_list");
expect(tool).toBeDefined();
if (!tool) {
throw new Error("missing sessions_list tool");
}
const result = await tool.execute("call2b", {});
const details = result.details as {
sessions?: Array<{
key?: string;
transcriptPath?: string;
}>;
};
const main = details.sessions?.find((session) => session.key === "main");
expect(typeof main?.transcriptPath).toBe("string");
expect(main?.transcriptPath).not.toContain("(multiple)");
expect(main?.transcriptPath).toContain(
path.join("agents", "main", "sessions", "sess-main.jsonl"),
);
});
it("sessions_history filters tool messages by default", async () => {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };

View File

@@ -1,7 +1,6 @@
import path from "node:path";
import { Type } from "@sinclair/typebox";
import { loadConfig } from "../../config/config.js";
import { resolveSessionFilePath } from "../../config/sessions.js";
import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../../config/sessions.js";
import { callGateway } from "../../gateway/call.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import type { AnyAgentTool } from "./common.js";
@@ -156,13 +155,14 @@ export function createSessionsListTool(opts?: {
let transcriptPath: string | undefined;
if (sessionId && storePath) {
try {
const sessionPathOpts = resolveSessionFilePathOptions({
agentId: resolveAgentIdFromSessionKey(key),
storePath,
});
transcriptPath = resolveSessionFilePath(
sessionId,
sessionFile ? { sessionFile } : undefined,
{
agentId: resolveAgentIdFromSessionKey(key),
sessionsDir: path.dirname(storePath),
},
sessionPathOpts,
);
} catch {
transcriptPath = undefined;

View File

@@ -39,13 +39,15 @@ export type SessionFilePathOptions = {
sessionsDir?: string;
};
const MULTI_STORE_PATH_SENTINEL = "(multiple)";
export function resolveSessionFilePathOptions(params: {
agentId?: string;
storePath?: string;
}): SessionFilePathOptions | undefined {
const agentId = params.agentId?.trim();
const storePath = params.storePath?.trim();
if (storePath) {
if (storePath && storePath !== MULTI_STORE_PATH_SENTINEL) {
const sessionsDir = path.dirname(path.resolve(storePath));
return agentId ? { sessionsDir, agentId } : { sessionsDir };
}

View File

@@ -13,6 +13,7 @@ import {
import type { SessionConfig } from "../types.base.js";
import {
resolveSessionFilePath,
resolveSessionFilePathOptions,
resolveSessionTranscriptPathInDir,
validateSessionId,
} from "./paths.js";
@@ -68,6 +69,13 @@ describe("session path safety", () => {
expect(resolved).toBe(path.resolve(sessionsDir, "sess-1.jsonl"));
});
it("ignores multi-store sentinel paths when deriving session file options", () => {
expect(resolveSessionFilePathOptions({ agentId: "worker", storePath: "(multiple)" })).toEqual({
agentId: "worker",
});
expect(resolveSessionFilePathOptions({ storePath: "(multiple)" })).toBeUndefined();
});
it("accepts symlink-alias session paths that resolve under the sessions dir", () => {
if (process.platform === "win32") {
return;