diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index 7554e416bdc..abac6e4d232 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -264,6 +264,9 @@ The remaining cleanup is mostly consolidation and deletion: - Bootstrap continuation detection now checks SQLite transcript locators through `hasCompletedBootstrapTranscriptTurn`; it no longer exposes a session-file-shaped helper name. +- Embedded-runner tests now use virtual SQLite transcript locators, and opening + a new locator without a duplicate `sessionId` uses the locator's session id + as the database row identity. - Memory indexing helpers now use SQLite transcript terminology end to end: host exports list/build session transcript entries, targeted sync queues `sessionTranscripts`, and QMD/builtin indexers no longer expose session-file diff --git a/src/agents/pi-embedded-runner.e2e.test.ts b/src/agents/pi-embedded-runner.e2e.test.ts index 365e26f7047..c8751c3d564 100644 --- a/src/agents/pi-embedded-runner.e2e.test.ts +++ b/src/agents/pi-embedded-runner.e2e.test.ts @@ -1,6 +1,11 @@ import path from "node:path"; import "./test-helpers/fast-coding-tools.js"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { + createSqliteSessionTranscriptLocator, + parseSqliteSessionTranscriptLocator, +} from "../config/sessions/paths.js"; +import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { buildEmbeddedRunnerAssistant, cleanupEmbeddedPiRunnerTestWorkspace, @@ -158,18 +163,27 @@ let agentDir: string; let workspaceDir: string; let sessionCounter = 0; let runCounter = 0; +let previousStateDir: string | undefined; beforeAll(async () => { vi.useRealTimers(); vi.resetModules(); installRunEmbeddedMocks(); - ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js")); - ({ SessionManager } = await import("./transcript/session-transcript-contract.js")); e2eWorkspace = await createEmbeddedPiRunnerTestWorkspace("openclaw-embedded-agent-"); ({ agentDir, workspaceDir } = e2eWorkspace); + previousStateDir = process.env.OPENCLAW_STATE_DIR; + process.env.OPENCLAW_STATE_DIR = e2eWorkspace.stateDir; + ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js")); + ({ SessionManager } = await import("./transcript/session-transcript-contract.js")); }, 180_000); afterAll(async () => { + closeOpenClawStateDatabaseForTest(); + if (previousStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previousStateDir; + } await cleanupEmbeddedPiRunnerTestWorkspace(e2eWorkspace); e2eWorkspace = undefined; }); @@ -195,8 +209,13 @@ beforeEach(() => { const nextSessionFile = () => { sessionCounter += 1; - return path.join(workspaceDir, `session-${sessionCounter}.jsonl`); + return createSqliteSessionTranscriptLocator({ + agentId: "test", + sessionId: `session-${sessionCounter}`, + }); }; +const sessionIdFromLocator = (sessionFile: string) => + parseSqliteSessionTranscriptLocator(sessionFile)?.sessionId ?? "session:test"; const nextRunId = (prefix = "run-embedded-test") => `${prefix}-${++runCounter}`; const nextSessionKey = () => `agent:test:embedded:${nextRunId("session-key")}`; @@ -220,7 +239,7 @@ const runWithOrphanedSingleUserMessage = async (text: string, sessionKey: string const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); return await runEmbeddedPiAgent({ - sessionId: "session:test", + sessionId: sessionIdFromLocator(sessionFile), sessionKey, sessionFile, workspaceDir, @@ -272,7 +291,7 @@ const runDefaultEmbeddedTurn = async (sessionFile: string, prompt: string, sessi }), ); await runEmbeddedPiAgent({ - sessionId: "session:test", + sessionId: sessionIdFromLocator(sessionFile), sessionKey, sessionFile, workspaceDir, @@ -482,6 +501,7 @@ describe("runEmbeddedPiAgent", () => { it("disposes bundle MCP once when a one-shot local run completes", async () => { const sessionFile = nextSessionFile(); + const sessionId = sessionIdFromLocator(sessionFile); const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); const sessionKey = nextSessionKey(); runEmbeddedAttemptMock.mockResolvedValueOnce( @@ -494,7 +514,7 @@ describe("runEmbeddedPiAgent", () => { ); await runEmbeddedPiAgent({ - sessionId: "session:test", + sessionId, sessionKey, sessionFile, workspaceDir, @@ -511,12 +531,13 @@ describe("runEmbeddedPiAgent", () => { expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(1); expect(disposeSessionMcpRuntimeMock).toHaveBeenCalledTimes(1); - expect(disposeSessionMcpRuntimeMock).toHaveBeenCalledWith("session:test"); + expect(disposeSessionMcpRuntimeMock).toHaveBeenCalledWith(sessionId); }); it("preserves bundle MCP state across retries within one local run", async () => { refreshRuntimeAuthOnFirstPromptError = true; const sessionFile = nextSessionFile(); + const sessionId = sessionIdFromLocator(sessionFile); const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); const sessionKey = nextSessionKey(); runEmbeddedAttemptMock @@ -537,7 +558,7 @@ describe("runEmbeddedPiAgent", () => { }); const result = await runEmbeddedPiAgent({ - sessionId: "session:test", + sessionId, sessionKey, sessionFile, workspaceDir, @@ -555,7 +576,7 @@ describe("runEmbeddedPiAgent", () => { expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2); expect(result.payloads?.[0]).toMatchObject({ text: "ok" }); expect(disposeSessionMcpRuntimeMock).toHaveBeenCalledTimes(1); - expect(disposeSessionMcpRuntimeMock).toHaveBeenCalledWith("session:test"); + expect(disposeSessionMcpRuntimeMock).toHaveBeenCalledWith(sessionId); }); it("retries a planning-only GPT turn once with an act-now steer", async () => { @@ -593,7 +614,7 @@ describe("runEmbeddedPiAgent", () => { }); const result = await runEmbeddedPiAgent({ - sessionId: "session:test", + sessionId: sessionIdFromLocator(sessionFile), sessionKey, sessionFile, workspaceDir, @@ -622,7 +643,7 @@ describe("runEmbeddedPiAgent", () => { ); await expect( runEmbeddedPiAgent({ - sessionId: "session:test", + sessionId: sessionIdFromLocator(sessionFile), sessionKey, sessionFile, workspaceDir, @@ -669,7 +690,6 @@ describe("runEmbeddedPiAgent", () => { usage: createMockUsage(1, 1), timestamp: Date.now(), }); - await runDefaultEmbeddedTurn(sessionFile, "hello", sessionKey); const messages = await readSessionMessages(sessionFile); diff --git a/src/agents/test-helpers/pi-embedded-runner-e2e-fixtures.ts b/src/agents/test-helpers/pi-embedded-runner-e2e-fixtures.ts index 5038b03044a..6916fab3afd 100644 --- a/src/agents/test-helpers/pi-embedded-runner-e2e-fixtures.ts +++ b/src/agents/test-helpers/pi-embedded-runner-e2e-fixtures.ts @@ -9,6 +9,7 @@ import type { EmbeddedRunAttemptResult } from "../pi-embedded-runner/run/types.j export type EmbeddedPiRunnerTestWorkspace = { tempRoot: string; agentDir: string; + stateDir: string; workspaceDir: string; }; @@ -17,10 +18,12 @@ export async function createEmbeddedPiRunnerTestWorkspace( ): Promise { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); const agentDir = path.join(tempRoot, "agent"); + const stateDir = path.join(tempRoot, "state"); const workspaceDir = path.join(tempRoot, "workspace"); await fs.mkdir(agentDir, { recursive: true }); + await fs.mkdir(stateDir, { recursive: true }); await fs.mkdir(workspaceDir, { recursive: true }); - return { tempRoot, agentDir, workspaceDir }; + return { tempRoot, agentDir, stateDir, workspaceDir }; } export async function cleanupEmbeddedPiRunnerTestWorkspace( diff --git a/src/agents/transcript/session-manager.test.ts b/src/agents/transcript/session-manager.test.ts index 48441ca5d68..6734c027df0 100644 --- a/src/agents/transcript/session-manager.test.ts +++ b/src/agents/transcript/session-manager.test.ts @@ -127,6 +127,33 @@ describe("TranscriptSessionManager", () => { ]); }); + it("uses the virtual sqlite transcript locator session id when no explicit id is supplied", async () => { + await makeTempSessionFile(); + const sessionFile = createSqliteSessionTranscriptLocator({ + agentId: "main", + sessionId: "locator-session", + }); + + const sessionManager = openTranscriptSessionManager({ + sessionFile, + cwd: "/tmp/workspace", + }); + sessionManager.appendMessage({ role: "user", content: "seed", timestamp: 1 }); + + expect(sessionManager.getSessionId()).toBe("locator-session"); + expect(readSessionEntries(sessionFile)).toMatchObject([ + { + type: "session", + id: "locator-session", + cwd: "/tmp/workspace", + }, + { + type: "message", + message: { role: "user", content: "seed" }, + }, + ]); + }); + it("creates, branches, lists, and forks default sessions with virtual sqlite locators", async () => { await makeTempSessionFile(); const sessionManager = SessionManager.create("/tmp/sqlite-workspace"); diff --git a/src/agents/transcript/session-manager.ts b/src/agents/transcript/session-manager.ts index 12badda1961..f9c71f17f16 100644 --- a/src/agents/transcript/session-manager.ts +++ b/src/agents/transcript/session-manager.ts @@ -140,7 +140,7 @@ function loadTranscriptState(params: { sessionFile: string; sessionId?: string; } const header = createSessionHeader({ - id: params.sessionId, + id: params.sessionId ?? scope.sessionId, cwd: params.cwd ?? process.cwd(), }); const state = new TranscriptState({ header, entries: [] });