diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7729f34..b5c716810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 753426a4c..4baa4794a 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -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 }; diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index bf16bbff3..73a25a681 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -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; diff --git a/src/config/sessions/paths.ts b/src/config/sessions/paths.ts index 0d3c0d6a2..e3e9d10b6 100644 --- a/src/config/sessions/paths.ts +++ b/src/config/sessions/paths.ts @@ -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 }; } diff --git a/src/config/sessions/sessions.test.ts b/src/config/sessions/sessions.test.ts index 10ac5a13b..4630bca0f 100644 --- a/src/config/sessions/sessions.test.ts +++ b/src/config/sessions/sessions.test.ts @@ -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;