diff --git a/src/config/sessions/transcript.test.ts b/src/config/sessions/transcript.test.ts new file mode 100644 index 00000000000..0a87b52bbc4 --- /dev/null +++ b/src/config/sessions/transcript.test.ts @@ -0,0 +1,112 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { SessionEntry } from "./types.js"; + +const state = vi.hoisted(() => ({ + sessionFile: "", + store: {} as Record, + appendMessage: vi.fn(() => "message-1"), +})); + +vi.mock("../io.js", () => ({ + loadConfig: () => ({ + agents: { + list: [{ id: "sunke", default: true }], + }, + }), +})); + +vi.mock("../../agents/agent-scope.js", () => ({ + resolveDefaultAgentId: () => "sunke", + resolveAgentWorkspaceDir: (_cfg: unknown, agentId: string) => + `/Users/admin/.openclaw/workspace-${agentId}`, +})); + +vi.mock("../../sessions/transcript-events.js", () => ({ + emitSessionTranscriptUpdate: vi.fn(), +})); + +vi.mock("./delivery-info.js", () => ({ + parseSessionThreadInfo: () => ({ baseSessionKey: undefined, threadId: undefined }), +})); + +vi.mock("./paths.js", () => ({ + resolveDefaultSessionStorePath: () => "/tmp/session-store.json", + resolveSessionFilePath: () => state.sessionFile, + resolveSessionFilePathOptions: () => ({ + agentId: "sunke", + sessionsDir: path.dirname(state.sessionFile), + }), + resolveSessionTranscriptPath: () => state.sessionFile, +})); + +vi.mock("./session-file.js", () => ({ + resolveAndPersistSessionFile: async () => ({ + sessionFile: state.sessionFile, + sessionEntry: state.store["agent:main:test:user-1"], + }), +})); + +vi.mock("./store.js", () => ({ + loadSessionStore: () => state.store, + normalizeStoreSessionKey: (value: string) => value, +})); + +vi.mock("@mariozechner/pi-coding-agent", () => ({ + CURRENT_SESSION_VERSION: 2, + SessionManager: { + open: () => ({ + appendMessage: state.appendMessage, + }), + }, +})); + +let appendAssistantMessageToSessionTranscript: typeof import("./transcript.js").appendAssistantMessageToSessionTranscript; +const originalCwd = process.cwd(); + +beforeEach(async () => { + vi.resetModules(); + state.appendMessage = vi.fn(() => "message-1"); + state.store = { + "agent:main:test:user-1": { + sessionId: "session-1", + updatedAt: Date.now(), + } as SessionEntry, + }; + + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-transcript-")); + const wrongWorkspace = path.join(tmpRoot, "wrong-workspace"); + fs.mkdirSync(wrongWorkspace, { recursive: true }); + process.chdir(wrongWorkspace); + + state.sessionFile = path.join(tmpRoot, "sessions", "session-1.jsonl"); + ({ appendAssistantMessageToSessionTranscript } = await import("./transcript.js")); +}); + +afterEach(() => { + process.chdir(originalCwd); +}); + +describe("appendAssistantMessageToSessionTranscript", () => { + it("writes the session header cwd from the target agent workspace instead of process.cwd()", async () => { + const result = await appendAssistantMessageToSessionTranscript({ + agentId: "sunke", + sessionKey: "agent:main:test:user-1", + text: "hello", + }); + + expect(result).toEqual({ + ok: true, + sessionFile: state.sessionFile, + messageId: "message-1", + }); + + const [headerLine] = fs.readFileSync(state.sessionFile, "utf-8").split(/\r?\n/); + const header = JSON.parse(headerLine) as { cwd?: string }; + + expect(header.cwd).toBe("/Users/admin/.openclaw/workspace-sunke"); + expect(header.cwd).not.toBe(process.cwd()); + }); +}); diff --git a/src/config/sessions/transcript.ts b/src/config/sessions/transcript.ts index aba99d02945..3ee4d39fcb6 100644 --- a/src/config/sessions/transcript.ts +++ b/src/config/sessions/transcript.ts @@ -1,7 +1,9 @@ import fs from "node:fs"; import path from "node:path"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; +import { resolveDefaultAgentId, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; +import { loadConfig } from "../io.js"; import { parseSessionThreadInfo } from "./delivery-info.js"; import { resolveDefaultSessionStorePath, @@ -67,6 +69,7 @@ export function resolveMirroredTranscriptText(params: { async function ensureSessionHeader(params: { sessionFile: string; sessionId: string; + cwd: string; }): Promise { if (fs.existsSync(params.sessionFile)) { return; @@ -77,7 +80,7 @@ async function ensureSessionHeader(params: { version: CURRENT_SESSION_VERSION, id: params.sessionId, timestamp: new Date().toISOString(), - cwd: process.cwd(), + cwd: params.cwd, }; await fs.promises.writeFile(params.sessionFile, `${JSON.stringify(header)}\n`, { encoding: "utf-8", @@ -160,6 +163,13 @@ export async function appendAssistantMessageToSessionTranscript(params: { return { ok: false, reason: `unknown sessionKey: ${sessionKey}` }; } + const cfg = loadConfig(); + const agentId = + typeof params.agentId === "string" && params.agentId.trim() + ? params.agentId.trim() + : resolveDefaultAgentId(cfg); + const sessionCwd = resolveAgentWorkspaceDir(cfg, agentId); + let sessionFile: string; try { const resolvedSessionFile = await resolveAndPersistSessionFile({ @@ -179,7 +189,11 @@ export async function appendAssistantMessageToSessionTranscript(params: { }; } - await ensureSessionHeader({ sessionFile, sessionId: entry.sessionId }); + await ensureSessionHeader({ + sessionFile, + sessionId: entry.sessionId, + cwd: sessionCwd, + }); const existingMessageId = params.idempotencyKey ? await transcriptHasIdempotencyKey(sessionFile, params.idempotencyKey)