diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 4fe94914ff6..b00dcd969f8 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -8,6 +8,7 @@ import { hasNonzeroUsage } from "../../agents/usage.js"; import { resolveAgentIdFromSessionKey, resolveSessionFilePath, + resolveSessionFilePathOptions, resolveSessionTranscriptPath, type SessionEntry, updateSessionStore, @@ -324,7 +325,11 @@ export async function runReplyAgent(params: { defaultRuntime.error(buildLogMessage(nextSessionId)); if (cleanupTranscripts && prevSessionId) { const transcriptCandidates = new Set(); - const resolved = resolveSessionFilePath(prevSessionId, prevEntry, { agentId }); + const resolved = resolveSessionFilePath( + prevSessionId, + prevEntry, + resolveSessionFilePathOptions({ agentId, storePath }), + ); if (resolved) { transcriptCandidates.add(resolved); } diff --git a/src/auto-reply/reply/commands-export-session.ts b/src/auto-reply/reply/commands-export-session.ts index 10d039741aa..5b560e4f269 100644 --- a/src/auto-reply/reply/commands-export-session.ts +++ b/src/auto-reply/reply/commands-export-session.ts @@ -6,6 +6,7 @@ import { SessionManager } from "@mariozechner/pi-coding-agent"; import { resolveDefaultSessionStorePath, resolveSessionFilePath, + resolveSessionFilePathOptions, } from "../../config/sessions/paths.js"; import { loadSessionStore } from "../../config/sessions/store.js"; import type { SessionEntry } from "../../config/sessions/types.js"; @@ -126,10 +127,11 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro let sessionFile: string; try { - sessionFile = resolveSessionFilePath(entry.sessionId, entry, { - agentId: params.agentId, - sessionsDir: path.dirname(storePath), - }); + sessionFile = resolveSessionFilePath( + entry.sessionId, + entry, + resolveSessionFilePathOptions({ agentId: params.agentId, storePath }), + ); } catch (err) { return { text: `❌ Failed to resolve session file: ${err instanceof Error ? err.message : String(err)}`, diff --git a/src/commands/agent.e2e.test.ts b/src/commands/agent.e2e.test.ts index 56c24571c4e..3d885617a75 100644 --- a/src/commands/agent.e2e.test.ts +++ b/src/commands/agent.e2e.test.ts @@ -5,10 +5,12 @@ import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; import "../cron/isolated-agent.mocks.js"; import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import * as cliRunnerModule from "../agents/cli-runner.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import type { OpenClawConfig } from "../config/config.js"; import * as configModule from "../config/config.js"; +import * as sessionsModule from "../config/sessions.js"; import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createPluginRuntime } from "../plugins/runtime/index.js"; @@ -25,6 +27,7 @@ const runtime: RuntimeEnv = { }; const configSpy = vi.spyOn(configModule, "loadConfig"); +const runCliAgentSpy = vi.spyOn(cliRunnerModule, "runCliAgent"); async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase(fn, { prefix: "openclaw-agent-" }); @@ -64,6 +67,13 @@ function writeSessionStoreSeed( beforeEach(() => { vi.clearAllMocks(); + runCliAgentSpy.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { + durationMs: 5, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + } as never); vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "ok" }], meta: { @@ -131,6 +141,28 @@ describe("agentCommand", () => { }); }); + it("resolves resumed session transcript path from custom session store directory", async () => { + await withTempHome(async (home) => { + const customStoreDir = path.join(home, "custom-state"); + const store = path.join(customStoreDir, "sessions.json"); + writeSessionStoreSeed(store, {}); + mockConfig(home, store); + const resolveSessionFilePathSpy = vi.spyOn(sessionsModule, "resolveSessionFilePath"); + + await agentCommand({ message: "resume me", sessionId: "session-custom-123" }, runtime); + + const matchingCall = resolveSessionFilePathSpy.mock.calls.find( + (call) => call[0] === "session-custom-123", + ); + expect(matchingCall?.[2]).toEqual( + expect.objectContaining({ + agentId: "main", + sessionsDir: customStoreDir, + }), + ); + }); + }); + it("does not duplicate agent events from embedded runs", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 576124bd81c..314b2948b0c 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import { listAgentIds, resolveAgentDir, @@ -45,6 +44,7 @@ import { resolveAndPersistSessionFile, resolveAgentIdFromSessionKey, resolveSessionFilePath, + resolveSessionFilePathOptions, resolveSessionTranscriptPath, type SessionEntry, updateSessionStore, @@ -510,10 +510,11 @@ export async function agentCommand( }); } } - let sessionFile = resolveSessionFilePath(sessionId, sessionEntry, { + const sessionPathOpts = resolveSessionFilePathOptions({ agentId: sessionAgentId, - sessionsDir: path.dirname(storePath), + storePath, }); + let sessionFile = resolveSessionFilePath(sessionId, sessionEntry, sessionPathOpts); if (sessionStore && sessionKey) { const threadIdFromSessionKey = parseSessionThreadInfo(sessionKey).threadId; const fallbackSessionFile = !sessionEntry?.sessionFile @@ -529,8 +530,8 @@ export async function agentCommand( sessionStore, storePath, sessionEntry, - agentId: sessionAgentId, - sessionsDir: path.dirname(storePath), + agentId: sessionPathOpts?.agentId, + sessionsDir: sessionPathOpts?.sessionsDir, fallbackSessionFile, }); sessionFile = resolvedSessionFile.sessionFile; diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index a62fcfb3108..d5beae1cec6 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -8,6 +8,7 @@ import { loadSessionStore, resolveMainSessionKey, resolveSessionFilePath, + resolveSessionFilePathOptions, resolveSessionTranscriptsDirForAgent, resolveStorePath, } from "../config/sessions.js"; @@ -386,6 +387,7 @@ export async function noteStateIntegrity( } const store = loadSessionStore(storePath); + const sessionPathOpts = resolveSessionFilePathOptions({ agentId, storePath }); const entries = Object.entries(store).filter(([, entry]) => entry && typeof entry === "object"); if (entries.length > 0) { const recent = entries @@ -401,9 +403,7 @@ export async function noteStateIntegrity( if (!sessionId) { return false; } - const transcriptPath = resolveSessionFilePath(sessionId, entry, { - agentId, - }); + const transcriptPath = resolveSessionFilePath(sessionId, entry, sessionPathOpts); return !existsFile(transcriptPath); }); if (missing.length > 0) { @@ -415,7 +415,11 @@ export async function noteStateIntegrity( const mainKey = resolveMainSessionKey(cfg); const mainEntry = store[mainKey]; if (mainEntry?.sessionId) { - const transcriptPath = resolveSessionFilePath(mainEntry.sessionId, mainEntry, { agentId }); + const transcriptPath = resolveSessionFilePath( + mainEntry.sessionId, + mainEntry, + sessionPathOpts, + ); if (!existsFile(transcriptPath)) { warnings.push( `- Main session transcript missing (${shortenHomePath(transcriptPath)}). History will appear to reset.`,