mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-16 18:34:18 +00:00
refactor: use sqlite locators for transient sessions
This commit is contained in:
@@ -200,6 +200,10 @@ The remaining cleanup is mostly consolidation and deletion:
|
||||
locators to embedded agents instead of creating temporary or persisted
|
||||
`session.jsonl` files under plugin state. The old `transcriptDir` option is
|
||||
now a compatibility no-op.
|
||||
- One-off slug generation and Crestodian planner runs now use virtual SQLite
|
||||
transcript locators instead of creating temporary `session.jsonl` files.
|
||||
`SessionManager.open()` preserves those locators instead of resolving them as
|
||||
filesystem paths.
|
||||
- Parent transcript fork decisions and fork creation no longer accept
|
||||
`storePath` or `sessionsDir`; they use `{agentId, sessionId}` SQLite
|
||||
transcript scope and derive any retained path metadata from the parent
|
||||
|
||||
@@ -223,6 +223,7 @@ describe("discord native /think autocomplete", () => {
|
||||
agentId: "main",
|
||||
sessionKey: SESSION_KEY,
|
||||
entry: {
|
||||
sessionId: "think-dm",
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: "openai-codex",
|
||||
modelOverride: "gpt-5.4",
|
||||
@@ -322,6 +323,7 @@ describe("discord native /think autocomplete", () => {
|
||||
agentId: "main",
|
||||
sessionKey: SESSION_KEY,
|
||||
entry: {
|
||||
sessionId: "think-guild",
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-opus-4-7",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
|
||||
type QaChannelActionConfig = {
|
||||
messages?: boolean;
|
||||
reactions?: boolean;
|
||||
@@ -32,13 +34,10 @@ type QaChannelConfig = QaChannelAccountConfig & {
|
||||
defaultAccount?: string;
|
||||
};
|
||||
|
||||
export type CoreConfig = {
|
||||
channels?: {
|
||||
export type CoreConfig = OpenClawConfig & {
|
||||
channels?: OpenClawConfig["channels"] & {
|
||||
"qa-channel"?: QaChannelConfig;
|
||||
};
|
||||
session?: {
|
||||
store?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ResolvedQaChannelAccount = {
|
||||
|
||||
@@ -190,7 +190,6 @@ async function resolveTelegramCommandSessionFile(params: {
|
||||
const persisted = await resolveAndPersistSessionFile({
|
||||
sessionId,
|
||||
sessionKey: resolved.normalizedKey,
|
||||
sessionStore: resolved.existing ? { [resolved.normalizedKey]: resolved.existing } : {},
|
||||
sessionEntry: resolved.existing,
|
||||
agentId: params.agentId,
|
||||
fallbackSessionFile,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
import type { VoiceCallTtsConfig } from "./config.js";
|
||||
|
||||
export type CoreConfig = {
|
||||
session?: {
|
||||
store?: string;
|
||||
};
|
||||
messages?: {
|
||||
export type CoreConfig = OpenClawConfig & {
|
||||
messages?: OpenClawConfig["messages"] & {
|
||||
tts?: VoiceCallTtsConfig;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type CoreAgentDeps = OpenClawPluginApi["runtime"]["agent"];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSessionEntry, upsertSessionEntry } from "../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
import {
|
||||
normalizeStoredOverrideModel,
|
||||
@@ -31,7 +32,7 @@ function readLiveSessionEntry(params: {
|
||||
}
|
||||
|
||||
export function resolveLiveSessionModelSelection(params: {
|
||||
cfg?: { session?: { store?: string } } | undefined;
|
||||
cfg?: OpenClawConfig | undefined;
|
||||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
defaultProvider: string;
|
||||
@@ -153,7 +154,7 @@ export function shouldTrackPersistedLiveSessionModelSelection(
|
||||
* user-initiated `/model` switches and system-initiated fallback rotations.
|
||||
*/
|
||||
export function shouldSwitchToLiveModel(params: {
|
||||
cfg?: { session?: { store?: string } } | undefined;
|
||||
cfg?: OpenClawConfig | undefined;
|
||||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
defaultProvider: string;
|
||||
@@ -210,7 +211,7 @@ export function shouldSwitchToLiveModel(params: {
|
||||
* subsequent retry iterations do not re-trigger the switch.
|
||||
*/
|
||||
export async function clearLiveModelSwitchPending(params: {
|
||||
cfg?: { session?: { store?: string } } | undefined;
|
||||
cfg?: OpenClawConfig | undefined;
|
||||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
}): Promise<void> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSqliteSessionTranscriptLocator } from "../../config/sessions/paths.js";
|
||||
import {
|
||||
loadSqliteSessionTranscriptEvents,
|
||||
resolveSqliteSessionTranscriptScopeForPath,
|
||||
@@ -92,6 +93,35 @@ describe("TranscriptSessionManager", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("opens virtual sqlite transcript locators without resolving them as filesystem paths", async () => {
|
||||
await makeTempSessionFile();
|
||||
const sessionFile = createSqliteSessionTranscriptLocator({
|
||||
agentId: "main",
|
||||
sessionId: "virtual-session",
|
||||
});
|
||||
|
||||
const sessionManager = openTranscriptSessionManager({
|
||||
sessionFile,
|
||||
sessionId: "virtual-session",
|
||||
cwd: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(sessionManager.getSessionFile()).toBe(sessionFile);
|
||||
expect(
|
||||
resolveSqliteSessionTranscriptScopeForPath({ transcriptPath: sessionFile }),
|
||||
).toMatchObject({
|
||||
agentId: "main",
|
||||
sessionId: "virtual-session",
|
||||
});
|
||||
expect(readSessionEntries(sessionFile)).toMatchObject([
|
||||
{
|
||||
type: "session",
|
||||
id: "virtual-session",
|
||||
cwd: "/tmp/workspace",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("persists initial user messages synchronously before the first assistant message", async () => {
|
||||
const sessionFile = await makeTempSessionFile();
|
||||
const sessionManager = openTranscriptSessionManager({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { isSqliteSessionTranscriptLocator } from "../../config/sessions/paths.js";
|
||||
import {
|
||||
appendSqliteSessionTranscriptEvent,
|
||||
listSqliteSessionTranscriptFiles,
|
||||
@@ -55,6 +56,18 @@ function resolveDefaultSessionDir(cwd: string): string {
|
||||
return path.join(os.homedir(), ".openclaw", "sessions", encodeSessionCwd(cwd));
|
||||
}
|
||||
|
||||
function normalizeSessionFileIdentifier(sessionFile: string): string {
|
||||
const trimmed = sessionFile.trim();
|
||||
return isSqliteSessionTranscriptLocator(trimmed) ? trimmed : path.resolve(trimmed);
|
||||
}
|
||||
|
||||
function resolveSessionDirForIdentifier(sessionFile: string, sessionDir?: string): string {
|
||||
if (sessionDir) {
|
||||
return path.resolve(sessionDir);
|
||||
}
|
||||
return isSqliteSessionTranscriptLocator(sessionFile) ? "" : path.dirname(sessionFile);
|
||||
}
|
||||
|
||||
function resolveAgentIdFromSessionPath(sessionFile: string): string {
|
||||
void sessionFile;
|
||||
return DEFAULT_AGENT_ID;
|
||||
@@ -102,7 +115,10 @@ function loadTranscriptState(params: { sessionFile: string; sessionId?: string;
|
||||
state: TranscriptState;
|
||||
scope: TranscriptSqliteScope;
|
||||
} {
|
||||
const transcriptPath = path.resolve(params.sessionFile);
|
||||
const sessionFile = params.sessionFile.trim();
|
||||
const transcriptPath = isSqliteSessionTranscriptLocator(sessionFile)
|
||||
? sessionFile
|
||||
: path.resolve(sessionFile);
|
||||
const existingScope = resolveSqliteSessionTranscriptScopeForPath({ transcriptPath });
|
||||
const scope = {
|
||||
agentId: existingScope?.agentId ?? resolveAgentIdFromSessionPath(transcriptPath),
|
||||
@@ -270,8 +286,10 @@ export class TranscriptSessionManager implements SessionManager {
|
||||
persist: boolean;
|
||||
sqliteScope?: TranscriptSqliteScope;
|
||||
}) {
|
||||
this.sessionFile = params.sessionFile ? path.resolve(params.sessionFile) : undefined;
|
||||
this.sessionDir = path.resolve(params.sessionDir);
|
||||
this.sessionFile = params.sessionFile
|
||||
? normalizeSessionFileIdentifier(params.sessionFile)
|
||||
: undefined;
|
||||
this.sessionDir = params.sessionDir ? path.resolve(params.sessionDir) : "";
|
||||
this.state = params.state;
|
||||
this.persist = params.persist;
|
||||
this.sqliteScope = params.sqliteScope;
|
||||
@@ -283,14 +301,14 @@ export class TranscriptSessionManager implements SessionManager {
|
||||
cwd?: string;
|
||||
sessionDir?: string;
|
||||
}): TranscriptSessionManager {
|
||||
const sessionFile = path.resolve(params.sessionFile);
|
||||
const sessionFile = normalizeSessionFileIdentifier(params.sessionFile);
|
||||
const loaded = loadTranscriptState({
|
||||
sessionFile,
|
||||
sessionId: params.sessionId,
|
||||
cwd: params.cwd,
|
||||
});
|
||||
return new TranscriptSessionManager({
|
||||
sessionDir: params.sessionDir ? path.resolve(params.sessionDir) : path.dirname(sessionFile),
|
||||
sessionDir: resolveSessionDirForIdentifier(sessionFile, params.sessionDir),
|
||||
sessionFile,
|
||||
persist: true,
|
||||
state: loaded.state,
|
||||
@@ -403,8 +421,8 @@ export class TranscriptSessionManager implements SessionManager {
|
||||
}
|
||||
|
||||
setSessionFile(sessionFile: string): void {
|
||||
this.sessionFile = path.resolve(sessionFile);
|
||||
this.sessionDir = path.dirname(this.sessionFile);
|
||||
this.sessionFile = normalizeSessionFileIdentifier(sessionFile);
|
||||
this.sessionDir = resolveSessionDirForIdentifier(this.sessionFile);
|
||||
this.persist = true;
|
||||
const loaded = loadTranscriptState({
|
||||
sessionFile: this.sessionFile,
|
||||
@@ -427,7 +445,7 @@ export class TranscriptSessionManager implements SessionManager {
|
||||
this.sqliteScope = {
|
||||
agentId: resolveAgentIdFromSessionPath(this.sessionFile),
|
||||
sessionId: header.id,
|
||||
transcriptPath: path.resolve(this.sessionFile),
|
||||
transcriptPath: normalizeSessionFileIdentifier(this.sessionFile),
|
||||
};
|
||||
persistFullTranscriptStateToSqlite(this.sqliteScope, this.state);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
prepareSimpleCompletionModelForAgent,
|
||||
} from "../agents/simple-completion-runtime.js";
|
||||
import { readConfigFileSnapshot } from "../config/config.js";
|
||||
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
|
||||
import { selectCrestodianLocalPlannerBackends } from "./assistant-backends.js";
|
||||
import {
|
||||
CRESTODIAN_ASSISTANT_MAX_TOKENS,
|
||||
@@ -181,8 +182,11 @@ async function runLocalRuntimePlanner(
|
||||
const tempDir = await (params.deps?.createTempDir ?? createTempPlannerDir)();
|
||||
try {
|
||||
const runId = `crestodian-planner-${randomUUID()}`;
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const sessionId = `${runId}-session`;
|
||||
const sessionFile = createSqliteSessionTranscriptLocator({
|
||||
agentId: "crestodian",
|
||||
sessionId,
|
||||
});
|
||||
const sessionKey = `temp:crestodian-planner:${runId}`;
|
||||
switch (backend.runner) {
|
||||
case "cli": {
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* LLM-based slug generator for session memory filenames
|
||||
*/
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
resolveDefaultAgentId,
|
||||
resolveAgentWorkspaceDir,
|
||||
@@ -13,6 +11,7 @@ import {
|
||||
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
@@ -35,16 +34,12 @@ export async function generateSlugViaLLM(params: {
|
||||
sessionContent: string;
|
||||
cfg: OpenClawConfig;
|
||||
}): Promise<string | null> {
|
||||
let tempSessionFile: string | null = null;
|
||||
|
||||
try {
|
||||
const agentId = resolveDefaultAgentId(params.cfg);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId);
|
||||
const agentDir = resolveAgentDir(params.cfg, agentId);
|
||||
|
||||
// Create a temporary session file for this one-off LLM call
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-slug-"));
|
||||
tempSessionFile = path.join(tempDir, "session.jsonl");
|
||||
const sessionId = `slug-generator-${randomUUID()}`;
|
||||
const sessionFile = createSqliteSessionTranscriptLocator({ agentId, sessionId });
|
||||
|
||||
const prompt = `Based on this conversation, generate a short 1-2 word filename slug (lowercase, hyphen-separated, no file extension).
|
||||
|
||||
@@ -60,10 +55,10 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
|
||||
const timeoutMs = resolveSlugGeneratorTimeoutMs(params.cfg);
|
||||
|
||||
const result = await runEmbeddedPiAgent({
|
||||
sessionId: `slug-generator-${Date.now()}`,
|
||||
sessionId,
|
||||
sessionKey: "temp:slug-generator",
|
||||
agentId,
|
||||
sessionFile: tempSessionFile,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
agentDir,
|
||||
config: params.cfg,
|
||||
@@ -95,14 +90,5 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
|
||||
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
||||
log.error(`Failed to generate slug: ${message}`);
|
||||
return null;
|
||||
} finally {
|
||||
// Clean up temporary session file
|
||||
if (tempSessionFile) {
|
||||
try {
|
||||
await fs.rm(path.dirname(tempSessionFile), { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user