mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-10 20:45:15 +00:00
refactor: drop session file path options
This commit is contained in:
@@ -113,6 +113,8 @@ Provider and channel execution paths must use the active runtime config snapshot
|
||||
**SQLite session row helpers** are under `api.runtime.agent.session`:
|
||||
|
||||
```typescript
|
||||
import { createSqliteSessionTranscriptLocator } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
|
||||
const entry = api.runtime.agent.session.getSessionEntry({ agentId, sessionKey });
|
||||
await api.runtime.agent.session.patchSessionEntry({
|
||||
agentId,
|
||||
@@ -122,7 +124,7 @@ Provider and channel execution paths must use the active runtime config snapshot
|
||||
thinkingLevel: "high",
|
||||
}),
|
||||
});
|
||||
const filePath = api.runtime.agent.session.resolveSessionFilePath(cfg, sessionId);
|
||||
const sessionFile = createSqliteSessionTranscriptLocator({ agentId, sessionId });
|
||||
```
|
||||
|
||||
Prefer row helpers such as `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, and `upsertSessionEntry(...)` for runtime writes. They route through the SQLite session row store and preserve concurrent updates. Legacy `sessions.json` parsing belongs in doctor/migration code, not plugin runtime paths.
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { createSqliteSessionTranscriptLocator } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
import type { SkillWorkshopConfig } from "./config.js";
|
||||
import { normalizeSkillName } from "./skills.js";
|
||||
@@ -247,13 +248,10 @@ export async function reviewTranscriptForProposal(params: {
|
||||
api: params.api,
|
||||
agentId: params.ctx.agentId,
|
||||
});
|
||||
const sessionFile = params.api.runtime.agent.session.resolveSessionFilePath(
|
||||
const sessionFile = createSqliteSessionTranscriptLocator({
|
||||
agentId: params.ctx.agentId,
|
||||
sessionId,
|
||||
undefined,
|
||||
{
|
||||
agentId: params.ctx.agentId,
|
||||
},
|
||||
);
|
||||
});
|
||||
const result = await params.api.runtime.agent.runEmbeddedPiAgent({
|
||||
sessionId,
|
||||
sessionKey: params.ctx.sessionKey,
|
||||
|
||||
@@ -248,7 +248,6 @@ describe("generateVoiceResponse", () => {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveAgentIdentity,
|
||||
resolveSessionFilePath,
|
||||
} = createAgentRuntime([{ text: '{"spoken":"Default agent."}' }]);
|
||||
const coreConfig = {} as CoreConfig;
|
||||
|
||||
@@ -265,16 +264,13 @@ describe("generateVoiceResponse", () => {
|
||||
expect(resolveAgentDir).toHaveBeenCalledWith(coreConfig, "main");
|
||||
expect(resolveAgentWorkspaceDir).toHaveBeenCalledWith(coreConfig, "main");
|
||||
expect(resolveAgentIdentity).toHaveBeenCalledWith(coreConfig, "main");
|
||||
expect(resolveSessionFilePath).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
|
||||
agentId: "main",
|
||||
});
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw/agents/main",
|
||||
agentId: "main",
|
||||
sandboxSessionKey: "agent:main:voice:15550001111",
|
||||
workspaceDir: "/tmp/openclaw/workspace/main",
|
||||
sessionFile: "/tmp/openclaw/main/sessions/session.jsonl",
|
||||
sessionFile: expect.stringMatching(/^sqlite-transcript:\/\/main\/.+\.jsonl$/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -286,7 +282,6 @@ describe("generateVoiceResponse", () => {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveAgentIdentity,
|
||||
resolveSessionFilePath,
|
||||
} = createAgentRuntime([{ text: '{"spoken":"Voice agent."}' }]);
|
||||
const coreConfig = {} as CoreConfig;
|
||||
|
||||
@@ -307,16 +302,13 @@ describe("generateVoiceResponse", () => {
|
||||
expect(resolveAgentDir).toHaveBeenCalledWith(coreConfig, "voice");
|
||||
expect(resolveAgentWorkspaceDir).toHaveBeenCalledWith(coreConfig, "voice");
|
||||
expect(resolveAgentIdentity).toHaveBeenCalledWith(coreConfig, "voice");
|
||||
expect(resolveSessionFilePath).toHaveBeenCalledWith(expect.any(String), expect.any(Object), {
|
||||
agentId: "voice",
|
||||
});
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw/agents/voice",
|
||||
agentId: "voice",
|
||||
sandboxSessionKey: "agent:voice:voice:15550001111",
|
||||
workspaceDir: "/tmp/openclaw/workspace/voice",
|
||||
sessionFile: "/tmp/openclaw/voice/sessions/session.jsonl",
|
||||
sessionFile: expect.stringMatching(/^sqlite-transcript:\/\/voice\/.+\.jsonl$/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
||||
import { createSqliteSessionTranscriptLocator } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { SessionEntry } from "../api.js";
|
||||
import { resolveVoiceCallSessionKey, type VoiceCallConfig } from "./config.js";
|
||||
@@ -255,8 +256,9 @@ export async function generateVoiceResponse(
|
||||
}
|
||||
const sessionId = sessionEntry.sessionId;
|
||||
|
||||
const sessionFile = agentRuntime.session.resolveSessionFilePath(sessionId, sessionEntry, {
|
||||
const sessionFile = createSqliteSessionTranscriptLocator({
|
||||
agentId,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
// Resolve thinking level
|
||||
|
||||
@@ -42,7 +42,6 @@ vi.mock("../../agents/skills/refresh.js", () => ({
|
||||
vi.mock("../../config/sessions.js", () => ({
|
||||
upsertSessionEntry: vi.fn(),
|
||||
resolveSessionFilePath: vi.fn(),
|
||||
resolveSessionFilePathOptions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/skills-remote.js", () => ({
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { isHeartbeatOkResponse, isHeartbeatUserMessage } from "../auto-reply/heartbeat-filter.js";
|
||||
import { formatFilesystemTimestamp } from "../config/sessions/artifacts.js";
|
||||
import { resolveMainSessionKey } from "../config/sessions/main-session.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
type resolveSessionFilePathOptions,
|
||||
} from "../config/sessions/paths.js";
|
||||
import { resolveSessionFilePath, type SessionFilePathOptions } from "../config/sessions/paths.js";
|
||||
import {
|
||||
deleteSessionEntry,
|
||||
getSessionEntry,
|
||||
@@ -183,7 +180,7 @@ export async function repairHeartbeatPoisonedMainSession(params: {
|
||||
cfg: OpenClawConfig;
|
||||
store: Record<string, SessionEntry>;
|
||||
stateDir: string;
|
||||
sessionPathOpts: ReturnType<typeof resolveSessionFilePathOptions>;
|
||||
sessionPathOpts: SessionFilePathOptions;
|
||||
prompter: DoctorPrompterLike;
|
||||
warnings: string[];
|
||||
changes: string[];
|
||||
|
||||
@@ -13,7 +13,6 @@ import { isPrimarySessionTranscriptFileName } from "../config/sessions/artifacts
|
||||
import { resolveMainSessionKey } from "../config/sessions/main-session.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
} from "../config/sessions/paths.js";
|
||||
import { listSessionEntries, upsertSessionEntry } from "../config/sessions/store.js";
|
||||
@@ -868,7 +867,7 @@ export async function noteStateIntegrity(
|
||||
const store = Object.fromEntries(
|
||||
listSessionEntries({ agentId, env }).map(({ sessionKey, entry }) => [sessionKey, entry]),
|
||||
);
|
||||
const sessionPathOpts = resolveSessionFilePathOptions({ agentId });
|
||||
const sessionPathOpts = { agentId };
|
||||
const entries = Object.entries(store).filter(([, entry]) => entry && typeof entry === "object");
|
||||
if (entries.length > 0) {
|
||||
const recent = entries
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
createSqliteSessionTranscriptLocator,
|
||||
deriveSessionKey,
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
resolveSessionKey,
|
||||
resolveSessionTranscriptPath,
|
||||
resolveSessionTranscriptsDir,
|
||||
@@ -76,16 +75,6 @@ describe("sessions", () => {
|
||||
);
|
||||
}
|
||||
|
||||
function expectedBot1FallbackSessionPath() {
|
||||
return path.join(
|
||||
path.resolve("/different/state"),
|
||||
"agents",
|
||||
"bot1",
|
||||
"sessions",
|
||||
"sess-1.jsonl",
|
||||
);
|
||||
}
|
||||
|
||||
function buildMainSessionEntry(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
sessionId: "sess-1",
|
||||
@@ -94,40 +83,6 @@ describe("sessions", () => {
|
||||
};
|
||||
}
|
||||
|
||||
async function createAgentSessionsLayout(label: string): Promise<{
|
||||
stateDir: string;
|
||||
mainSessionsDir: string;
|
||||
bot2SessionPath: string;
|
||||
outsidePath: string;
|
||||
}> {
|
||||
const stateDir = await createCaseDir(label);
|
||||
const mainSessionsDir = path.join(stateDir, "agents", "main", "sessions");
|
||||
const bot1SessionsDir = path.join(stateDir, "agents", "bot1", "sessions");
|
||||
const bot2SessionsDir = path.join(stateDir, "agents", "bot2", "sessions");
|
||||
await fs.mkdir(mainSessionsDir, { recursive: true });
|
||||
await fs.mkdir(bot1SessionsDir, { recursive: true });
|
||||
await fs.mkdir(bot2SessionsDir, { recursive: true });
|
||||
|
||||
const bot2SessionPath = path.join(bot2SessionsDir, "sess-1.jsonl");
|
||||
await fs.writeFile(bot2SessionPath, "{}", "utf-8");
|
||||
|
||||
const outsidePath = path.join(stateDir, "outside", "not-a-session.jsonl");
|
||||
await fs.mkdir(path.dirname(outsidePath), { recursive: true });
|
||||
await fs.writeFile(outsidePath, "{}", "utf-8");
|
||||
|
||||
return { stateDir, mainSessionsDir, bot2SessionPath, outsidePath };
|
||||
}
|
||||
|
||||
async function normalizePathForComparison(filePath: string): Promise<string> {
|
||||
const canonicalFile = await fs.realpath(filePath).catch(() => null);
|
||||
if (canonicalFile) {
|
||||
return canonicalFile;
|
||||
}
|
||||
const parentDir = path.dirname(filePath);
|
||||
const canonicalParent = await fs.realpath(parentDir).catch(() => parentDir);
|
||||
return path.join(canonicalParent, path.basename(filePath));
|
||||
}
|
||||
|
||||
const deriveSessionKeyCases = [
|
||||
{
|
||||
name: "returns normalized per-sender key",
|
||||
@@ -680,99 +635,51 @@ describe("sessions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves cross-agent absolute sessionFile paths", async () => {
|
||||
const { stateDir, bot2SessionPath } = await createAgentSessionsLayout("cross-agent");
|
||||
const sessionFile = withStateDir(stateDir, () =>
|
||||
// Agent bot1 resolves a sessionFile that belongs to agent bot2
|
||||
resolveSessionFilePath("sess-1", { sessionFile: bot2SessionPath }, { agentId: "bot1" }),
|
||||
);
|
||||
expect(await normalizePathForComparison(sessionFile)).toBe(
|
||||
await normalizePathForComparison(bot2SessionPath),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves cross-agent paths when OPENCLAW_STATE_DIR differs from stored paths", () => {
|
||||
it("does not reuse legacy cross-agent absolute sessionFile paths", () => {
|
||||
withStateDir(path.resolve("/different/state"), () => {
|
||||
const originalBase = path.resolve("/original/state");
|
||||
const bot2Session = path.join(originalBase, "agents", "bot2", "sessions", "sess-1.jsonl");
|
||||
// sessionFile was created under a different state dir than current env
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
"sess-1",
|
||||
{ sessionFile: bot2Session },
|
||||
{ agentId: "bot1" },
|
||||
);
|
||||
expect(sessionFile).toBe(bot2Session);
|
||||
expect(sessionFile).toBe(
|
||||
createSqliteSessionTranscriptLocator({ agentId: "bot1", sessionId: "sess-1" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when structural cross-root path traverses after sessions", () => {
|
||||
it("keeps matching SQLite transcript locators", () => {
|
||||
withStateDir(path.resolve("/different/state"), () => {
|
||||
const originalBase = path.resolve("/original/state");
|
||||
const unsafe = path.join(originalBase, "agents", "bot2", "sessions", "..", "..", "etc");
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
"sess-1",
|
||||
{ sessionFile: path.join(unsafe, "passwd") },
|
||||
{ agentId: "bot1" },
|
||||
);
|
||||
expect(sessionFile).toBe(expectedBot1FallbackSessionPath());
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when structural cross-root path nests under sessions", () => {
|
||||
withStateDir(path.resolve("/different/state"), () => {
|
||||
const originalBase = path.resolve("/original/state");
|
||||
const nested = path.join(
|
||||
originalBase,
|
||||
"agents",
|
||||
"bot2",
|
||||
"sessions",
|
||||
"nested",
|
||||
"sess-1.jsonl",
|
||||
);
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
"sess-1",
|
||||
{ sessionFile: nested },
|
||||
{ agentId: "bot1" },
|
||||
);
|
||||
expect(sessionFile).toBe(expectedBot1FallbackSessionPath());
|
||||
});
|
||||
});
|
||||
|
||||
it("resolveSessionFilePathOptions keeps explicit agentId alongside absolute sessions dir", () => {
|
||||
const sessionsDir = "/tmp/openclaw/agents/main/sessions";
|
||||
const resolved = resolveSessionFilePathOptions({
|
||||
agentId: "bot2",
|
||||
sessionsDir,
|
||||
});
|
||||
expect(resolved?.agentId).toBe("bot2");
|
||||
expect(resolved?.sessionsDir).toBe(path.resolve(sessionsDir));
|
||||
});
|
||||
|
||||
it("resolves sibling agent absolute sessionFile using alternate agentId from options", async () => {
|
||||
const { stateDir, mainSessionsDir, bot2SessionPath } =
|
||||
await createAgentSessionsLayout("sibling-agent");
|
||||
const sessionFile = withStateDir(stateDir, () => {
|
||||
const opts = resolveSessionFilePathOptions({
|
||||
agentId: "bot2",
|
||||
sessionsDir: mainSessionsDir,
|
||||
const locator = createSqliteSessionTranscriptLocator({
|
||||
agentId: "bot1",
|
||||
sessionId: "sess-1",
|
||||
});
|
||||
|
||||
return resolveSessionFilePath("sess-1", { sessionFile: bot2SessionPath }, opts);
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
"sess-1",
|
||||
{ sessionFile: locator },
|
||||
{ agentId: "bot1" },
|
||||
);
|
||||
expect(sessionFile).toBe(locator);
|
||||
});
|
||||
expect(await normalizePathForComparison(sessionFile)).toBe(
|
||||
await normalizePathForComparison(bot2SessionPath),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to derived transcript path when sessionFile is outside agent sessions directories", async () => {
|
||||
const { stateDir, outsidePath } = await createAgentSessionsLayout("outside-fallback");
|
||||
const sessionFile = withStateDir(stateDir, () =>
|
||||
resolveSessionFilePath("sess-1", { sessionFile: outsidePath }, { agentId: "bot1" }),
|
||||
);
|
||||
const expectedPath = path.join(stateDir, "agents", "bot1", "sessions", "sess-1.jsonl");
|
||||
expect(await normalizePathForComparison(sessionFile)).toBe(
|
||||
await normalizePathForComparison(expectedPath),
|
||||
);
|
||||
it("does not reuse SQLite transcript locators for a different agent", () => {
|
||||
withStateDir(path.resolve("/different/state"), () => {
|
||||
const bot2Locator = createSqliteSessionTranscriptLocator({
|
||||
agentId: "bot2",
|
||||
sessionId: "sess-1",
|
||||
});
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
"sess-1",
|
||||
{ sessionFile: bot2Locator },
|
||||
{ agentId: "bot1" },
|
||||
);
|
||||
expect(sessionFile).toBe(
|
||||
createSqliteSessionTranscriptLocator({ agentId: "bot1", sessionId: "sess-1" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("patchSessionEntry merges concurrent patches", async () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ export {
|
||||
isSqliteSessionTranscriptLocator,
|
||||
parseSqliteSessionTranscriptLocator,
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
resolveSessionTranscriptPath,
|
||||
resolveSessionTranscriptPathInDir,
|
||||
resolveSessionTranscriptsDir,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
createSqliteSessionTranscriptLocator,
|
||||
isSqliteSessionTranscriptLocator,
|
||||
type SessionFilePathOptions,
|
||||
} from "./paths.js";
|
||||
import { createSqliteSessionTranscriptLocator, isSqliteSessionTranscriptLocator } from "./paths.js";
|
||||
import {
|
||||
loadSqliteSessionTranscriptEvents,
|
||||
resolveSqliteSessionTranscriptScope,
|
||||
@@ -32,13 +28,11 @@ function parseTimestampMs(value: unknown): number | undefined {
|
||||
export function readSessionHeaderStartedAtMs(params: {
|
||||
entry: SessionLifecycleEntry | undefined;
|
||||
agentId?: string;
|
||||
pathOptions?: SessionFilePathOptions;
|
||||
}): number | undefined {
|
||||
const sessionId = params.entry?.sessionId?.trim();
|
||||
if (!sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
void params.pathOptions;
|
||||
const storedSessionFile = params.entry?.sessionFile?.trim();
|
||||
const sessionFile = isSqliteSessionTranscriptLocator(storedSessionFile)
|
||||
? storedSessionFile
|
||||
@@ -82,7 +76,6 @@ export function readSessionHeaderStartedAtMs(params: {
|
||||
export function resolveSessionLifecycleTimestamps(params: {
|
||||
entry: SessionLifecycleEntry | undefined;
|
||||
agentId?: string;
|
||||
pathOptions?: SessionFilePathOptions;
|
||||
}): { sessionStartedAt?: number; lastInteractionAt?: number } {
|
||||
const entry = params.entry;
|
||||
if (!entry) {
|
||||
@@ -94,7 +87,6 @@ export function resolveSessionLifecycleTimestamps(params: {
|
||||
readSessionHeaderStartedAtMs({
|
||||
entry,
|
||||
agentId: params.agentId,
|
||||
pathOptions: params.pathOptions,
|
||||
}),
|
||||
lastInteractionAt: resolveTimestamp(entry.lastInteractionAt),
|
||||
};
|
||||
|
||||
@@ -32,27 +32,8 @@ export function resolveSessionTranscriptsDirForAgent(
|
||||
|
||||
export type SessionFilePathOptions = {
|
||||
agentId?: string;
|
||||
sessionsDir?: string;
|
||||
};
|
||||
|
||||
export function resolveSessionFilePathOptions(params: {
|
||||
agentId?: string;
|
||||
sessionsDir?: string;
|
||||
}): SessionFilePathOptions | undefined {
|
||||
const agentId = params.agentId?.trim();
|
||||
const sessionsDir = params.sessionsDir?.trim();
|
||||
if (sessionsDir) {
|
||||
const resolvedSessionsDir = path.resolve(sessionsDir);
|
||||
return agentId
|
||||
? { sessionsDir: resolvedSessionsDir, agentId }
|
||||
: { sessionsDir: resolvedSessionsDir };
|
||||
}
|
||||
if (agentId) {
|
||||
return { agentId };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
||||
export const SQLITE_SESSION_TRANSCRIPT_LOCATOR_PREFIX = "sqlite-transcript://";
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { resolveSessionLifecycleTimestamps } from "./lifecycle.js";
|
||||
import {
|
||||
createSqliteSessionTranscriptLocator,
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
resolveSessionTranscriptPathInDir,
|
||||
validateSessionId,
|
||||
} from "./paths.js";
|
||||
@@ -57,19 +56,6 @@ describe("session path safety", () => {
|
||||
expect(resolved).toBe(createSqliteSessionTranscriptLocator({ sessionId: "sess-1" }));
|
||||
});
|
||||
|
||||
it("derives session file options from an explicit sessions dir", () => {
|
||||
expect(
|
||||
resolveSessionFilePathOptions({
|
||||
agentId: "worker",
|
||||
sessionsDir: "/tmp/openclaw/agents/worker/sessions",
|
||||
}),
|
||||
).toEqual({
|
||||
agentId: "worker",
|
||||
sessionsDir: path.resolve("/tmp/openclaw/agents/worker/sessions"),
|
||||
});
|
||||
expect(resolveSessionFilePathOptions({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses SQLite transcript locators instead of runtime JSONL paths by default", () => {
|
||||
expect(
|
||||
resolveSessionFilePath("sess-1", {
|
||||
|
||||
Reference in New Issue
Block a user