Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/memos-local-openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ const memosLocalPlugin = {
});
});

const summarizer = new Summarizer(ctx.config.summarizer, ctx.log);
const summarizer = new Summarizer(ctx.config.summarizer, ctx.log, undefined, ctx.stateDir);

api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);

Expand Down
10 changes: 10 additions & 0 deletions apps/memos-local-openclaw/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
};
}

export function getOpenClawHome(stateDir?: string): string {
if (stateDir && stateDir.trim().length > 0) return stateDir;
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
return path.join(home, ".openclaw");
}

export function getOpenClawConfigPath(stateDir?: string): string {
return path.join(getOpenClawHome(stateDir), "openclaw.json");
}

export function buildContext(
stateDir: string,
workspaceDir: string,
Expand Down
10 changes: 5 additions & 5 deletions apps/memos-local-openclaw/src/ingest/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from "fs";
import * as path from "path";
import type { SummarizerConfig, Logger } from "../../types";
import { getOpenClawConfigPath } from "../../config";
import { summarizeOpenAI, summarizeTaskOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
import type { FilterResult, DedupResult } from "./openai";
export type { FilterResult, DedupResult } from "./openai";
Expand All @@ -12,10 +12,9 @@ import { summarizeBedrock, summarizeTaskBedrock, judgeNewTopicBedrock, filterRel
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
* This serves as the final fallback when both strongCfg and plugin summarizer fail or are absent.
*/
function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
export function loadOpenClawFallbackConfig(log: Logger, stateDir?: string): SummarizerConfig | undefined {
try {
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
const cfgPath = getOpenClawConfigPath(stateDir);
if (!fs.existsSync(cfgPath)) return undefined;

const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
Expand Down Expand Up @@ -61,9 +60,10 @@ export class Summarizer {
private cfg: SummarizerConfig | undefined,
private log: Logger,
strongCfg?: SummarizerConfig,
stateDir?: string,
) {
this.strongCfg = strongCfg;
this.fallbackCfg = loadOpenClawFallbackConfig(log);
this.fallbackCfg = loadOpenClawFallbackConfig(log, stateDir);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion apps/memos-local-openclaw/src/ingest/task-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class TaskProcessor {
private ctx: PluginContext,
) {
const strongCfg = ctx.config.skillEvolution?.summarizer;
this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, strongCfg);
this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, strongCfg, ctx.stateDir);
}

onTaskCompleted(cb: (task: Task) => void): void {
Expand Down
2 changes: 1 addition & 1 deletion apps/memos-local-openclaw/src/ingest/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class IngestWorker {
private ctx: PluginContext,
) {
const strongCfg = ctx.config.skillEvolution?.summarizer;
this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, strongCfg);
this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, strongCfg, ctx.stateDir);
this.taskProcessor = new TaskProcessor(store, ctx);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/memos-local-openclaw/src/recall/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export class RecallEngine {
if (candidateSkills.length === 0) return [];

// LLM relevance judgment
const summarizer = new Summarizer(this.ctx.config.summarizer, this.ctx.log);
const summarizer = new Summarizer(this.ctx.config.summarizer, this.ctx.log, undefined, this.ctx.stateDir);
const relevantIndices = await this.judgeSkillRelevance(summarizer, query, candidateSkills);

return relevantIndices.map((idx) => {
Expand Down
9 changes: 4 additions & 5 deletions apps/memos-local-openclaw/src/shared/llm-call.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import * as fs from "fs";
import * as path from "path";
import type { SummarizerConfig, Logger, PluginContext } from "../types";
import { getOpenClawConfigPath } from "../config";

/**
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
* Final fallback when both strongCfg and plugin summarizer fail or are absent.
*/
export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
export function loadOpenClawFallbackConfig(log: Logger, stateDir?: string): SummarizerConfig | undefined {
try {
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
const cfgPath = getOpenClawConfigPath(stateDir);
if (!fs.existsSync(cfgPath)) return undefined;

const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
Expand Down Expand Up @@ -55,7 +54,7 @@ export function buildSkillConfigChain(ctx: PluginContext): SummarizerConfig[] {
const chain: SummarizerConfig[] = [];
const skillCfg = ctx.config.skillEvolution?.summarizer;
const pluginCfg = ctx.config.summarizer;
const fallbackCfg = loadOpenClawFallbackConfig(ctx.log);
const fallbackCfg = loadOpenClawFallbackConfig(ctx.log, ctx.stateDir);
if (skillCfg) chain.push(skillCfg);
if (pluginCfg && pluginCfg !== skillCfg) chain.push(pluginCfg);
if (fallbackCfg) chain.push(fallbackCfg);
Expand Down
9 changes: 4 additions & 5 deletions apps/memos-local-openclaw/src/viewer/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TaskProcessor } from "../ingest/task-processor";
import { RecallEngine } from "../recall/engine";
import { SkillEvolver } from "../skill/evolver";
import type { Logger, Chunk, PluginContext } from "../types";
import { getOpenClawConfigPath, getOpenClawHome } from "../config";
import { viewerHTML } from "./html";
import { v4 as uuid } from "uuid";

Expand Down Expand Up @@ -958,8 +959,7 @@ export class ViewerServer {
// ─── Config API ───

private getOpenClawConfigPath(): string {
const home = process.env.HOME || process.env.USERPROFILE || "";
return path.join(home, ".openclaw", "openclaw.json");
return getOpenClawConfigPath(this.ctx?.stateDir ?? this.dataDir);
}

private serveConfig(res: http.ServerResponse): void {
Expand Down Expand Up @@ -1049,8 +1049,7 @@ export class ViewerServer {
// ─── Migration: scan OpenClaw built-in memory ───

private getOpenClawHome(): string {
const home = process.env.HOME || process.env.USERPROFILE || "";
return path.join(home, ".openclaw");
return getOpenClawHome(this.ctx?.stateDir ?? this.dataDir);
}

private handleMigrateScan(res: http.ServerResponse): void {
Expand Down Expand Up @@ -1305,7 +1304,7 @@ export class ViewerServer {
strongCfg = pluginCfg.skillEvolution?.summarizer;
} catch { /* no config */ }

const summarizer = new Summarizer(summarizerCfg, this.log, strongCfg);
const summarizer = new Summarizer(summarizerCfg, this.log, strongCfg, this.ctx?.stateDir ?? this.dataDir);

// Phase 1: Import SQLite memory chunks
if (importSqlite) {
Expand Down
102 changes: 102 additions & 0 deletions apps/memos-local-openclaw/tests/profile-paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { buildContext, getOpenClawConfigPath, getOpenClawHome } from "../src/config";
import { loadOpenClawFallbackConfig as loadProviderFallbackConfig } from "../src/ingest/providers";
import { loadOpenClawFallbackConfig as loadSharedFallbackConfig, buildSkillConfigChain } from "../src/shared/llm-call";
import { ViewerServer } from "../src/viewer/server";

const noopLog = {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
};

function writeOpenClawConfig(stateDir: string, model: string): void {
fs.mkdirSync(stateDir, { recursive: true });
fs.writeFileSync(
path.join(stateDir, "openclaw.json"),
JSON.stringify({
agents: {
defaults: {
model: {
primary: `openai/${model}`,
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://example.com/v1",
apiKey: "test-key",
},
},
},
}),
"utf-8",
);
}

describe("profile-aware OpenClaw paths", () => {
let tmpDir: string;
let previousHome: string | undefined;
let previousUserProfile: string | undefined;
let profileStateDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "memos-profile-paths-"));
previousHome = process.env.HOME;
previousUserProfile = process.env.USERPROFILE;
process.env.HOME = tmpDir;
delete process.env.USERPROFILE;

writeOpenClawConfig(path.join(tmpDir, ".openclaw"), "default-model");
profileStateDir = path.join(tmpDir, ".openclaw-lucky");
writeOpenClawConfig(profileStateDir, "profile-model");
});

afterEach(() => {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = previousUserProfile;
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it("prefers provided stateDir over the default ~/.openclaw paths", () => {
expect(getOpenClawHome()).toBe(path.join(tmpDir, ".openclaw"));
expect(getOpenClawConfigPath()).toBe(path.join(tmpDir, ".openclaw", "openclaw.json"));
expect(getOpenClawHome(profileStateDir)).toBe(profileStateDir);
expect(getOpenClawConfigPath(profileStateDir)).toBe(path.join(profileStateDir, "openclaw.json"));
});

it("loads fallback model config from the active profile stateDir", () => {
const defaultCfg = loadProviderFallbackConfig(noopLog as any);
const profileCfg = loadProviderFallbackConfig(noopLog as any, profileStateDir);
const sharedCfg = loadSharedFallbackConfig(noopLog as any, profileStateDir);

expect(defaultCfg?.model).toBe("default-model");
expect(profileCfg?.model).toBe("profile-model");
expect(sharedCfg?.model).toBe("profile-model");
});

it("uses the profile stateDir in the skill chain and viewer helpers", () => {
const ctx = buildContext(profileStateDir, path.join(tmpDir, "workspace"), {}, noopLog as any);
const chain = buildSkillConfigChain(ctx);
const viewer = new ViewerServer({
store: {} as any,
embedder: {} as any,
port: 0,
log: noopLog as any,
dataDir: profileStateDir,
ctx,
});

expect(chain).toHaveLength(1);
expect(chain[0]?.model).toBe("profile-model");
expect((viewer as any).getOpenClawHome()).toBe(profileStateDir);
expect((viewer as any).getOpenClawConfigPath()).toBe(path.join(profileStateDir, "openclaw.json"));
});
});