Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
512d45a
fix(capture): skip "Review the conversation above" internal prompts
SonicBotMan Apr 22, 2026
78ae7a5
fix(memos-local-plugin): handle full endpoint paths in openai_compati…
liuhaibin0528 May 8, 2026
38fbcfc
Fix #1540: fix: (#1824)
Memtensor-AI Jun 14, 2026
576b408
Fix #1888: [Bug] test_system_parser.py: SystemParser.__init__() got a…
Memtensor-AI Jun 14, 2026
968930f
Fix #1525: [Bug] clean_json_response crashes with cryptic AttributeEr…
Memtensor-AI Jun 14, 2026
8bfef25
Fix #1901: share_cube_with_user passes swapped args to _validate_cube…
Memtensor-AI Jun 14, 2026
8aef4a6
fix(capture): skip orphan trace insert during dirty-reward recovery
chiefmojo May 28, 2026
d2ec89b
fix(bridge): extend startup poll window and prevent double-spawn
chiefmojo May 28, 2026
4afaa3c
fix(auth): restrict /api/v1/rpc session exemption to loopback callers
chiefmojo May 28, 2026
ca2b385
fix(memos): apply Memtensor-AI review feedback to PR #1817
fayenix May 28, 2026
0398e0e
Merge branch 'main' into pr/may27-fixes
chiefmojo Jul 4, 2026
257b2be
feat: route embedding input types by usage
RerankerGuo Jul 1, 2026
3e67ae8
refactor(mem-reader): remove dead remove_prompt_example branch in Str…
nuthalapativarun Jul 8, 2026
afada8c
docs: clarify API product model descriptions (#1810)
Lucas-FManager Jul 8, 2026
713ed02
fix: correct "monitering" typo in task_id field descriptions (#1931)
nuthalapativarun Jul 8, 2026
d9046d6
fix: quote OpenClaw FTS query terms (#2043)
RerankerGuo Jul 8, 2026
d2adf17
docs: fix Quick Start examples in Chat & Message API docs to match Me…
hiasky Jul 8, 2026
10014cf
docs: translate API reference pages (#2053)
RerankerGuo Jul 9, 2026
d7f15b0
fix: align Gemini viewer embedding test (#2050)
RerankerGuo Jul 9, 2026
c4ed7a2
fix: add durable Hermes provider link (#2051)
RerankerGuo Jul 9, 2026
e508065
fix: delimit memory context with XML boundary (#2052)
RerankerGuo Jul 9, 2026
98b0372
fix: load bridge package version without require (#1992)
RerankerGuo Jul 9, 2026
46dff5b
fix: log LLM error response bodies (#2012)
RerankerGuo Jul 9, 2026
2c48b49
fix(skill-crystallize): retry with context when LLM returns empty res…
34262315716 Jul 9, 2026
598b674
fix: avoid blocking OpenClaw plugin registration (#2044)
RerankerGuo Jul 9, 2026
edcd7c8
fix: bridge startup race, dirty-reward orphan traces, RPC loopback au…
syzsunshine219 Jul 9, 2026
de03ab2
Fix #2063: algorithm.lightweightMemory.enabled: true does not actuall…
Memtensor-AI Jul 9, 2026
a9338d8
feat: add configurable CJK keyword tokenization (#2040)
RerankerGuo Jul 9, 2026
43b8f2e
feat: route embedding input types by usage (#2024)
syzsunshine219 Jul 9, 2026
365bd8b
feat(memos-local): add data export endpoint and UI (closes #1474) (#1…
owensun6 Jul 9, 2026
347ce82
Merge branch 'dev-v2.0.23' into fix/openai-compatible-endpoint-path-d…
syzsunshine219 Jul 9, 2026
0f4d286
fix(memos-local-plugin): handle full endpoint paths in openai_compati…
syzsunshine219 Jul 9, 2026
11758c9
fix: add missing contracts.tools declaration for OpenClaw tool allowl…
saraphine001 Jul 9, 2026
8436e3f
fix: Disable thinking for qwen3 preference extractor
bittergreen Jul 8, 2026
88e4fa4
fix: align capture review prompt filter with main
Jul 9, 2026
7de6b07
Merge branch 'dev-v2.0.23' into fix/review-conversation-prompt-177687…
syzsunshine219 Jul 9, 2026
c739e9f
fix: ruff
bittergreen Jul 9, 2026
bcb711e
fix: Disable thinking for qwen3 preference extractor (#2079)
bittergreen Jul 9, 2026
8db255e
fix: tests for preference extractors (#2080)
bittergreen Jul 9, 2026
4053c25
feat(memos-local): make auto-recall Phase 1 search parameters configu…
yongren-del Jul 9, 2026
51ad29f
Merge branch 'dev-v2.0.23' into fix/review-conversation-prompt-177687…
syzsunshine219 Jul 9, 2026
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
111 changes: 1 addition & 110 deletions apps/memos-local-openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "@sinclair/typebox";
import * as fs from "fs";
import * as path from "path";
import { createRequire } from "node:module";
import { fileURLToPath } from "url";
import { buildContext } from "./src/config";
import type { HostModelsConfig } from "./src/openclaw-api";
Expand Down Expand Up @@ -225,115 +224,7 @@ const memosLocalPlugin = {
}

const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const localRequire = createRequire(import.meta.url);
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";

function detectPluginDir(startDir: string): string {
let cur = startDir;
for (let i = 0; i < 6; i++) {
const pkg = path.join(cur, "package.json");
if (fs.existsSync(pkg)) return cur;
const parent = path.dirname(cur);
if (parent === cur) break;
cur = parent;
}
return startDir;
}

const pluginDir = detectPluginDir(moduleDir);

function runNpm(args: string[]) {
const { spawnSync } = localRequire("child_process") as typeof import("node:child_process");
return spawnSync(npmCmd, args, {
cwd: pluginDir,
stdio: "pipe",
shell: false,
timeout: 120_000,
});
}

let sqliteReady = false;

function trySqliteLoad(): boolean {
try {
const resolved = localRequire.resolve("better-sqlite3", { paths: [pluginDir] });
const resolvedReal = fs.existsSync(resolved) ? fs.realpathSync.native(resolved) : resolved;
if (!isPathInside(pluginDir, resolvedReal)) {
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
return false;
}
localRequire(resolvedReal);
return true;
} catch {
return false;
}
}

sqliteReady = trySqliteLoad();

if (!sqliteReady) {
api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-rebuild ...`);

try {
const rebuildResult = runNpm(["rebuild", "better-sqlite3"]);

const stdout = rebuildResult.stdout?.toString() || "";
const stderr = rebuildResult.stderr?.toString() || "";
if (stdout) api.logger.info(`memos-local: rebuild stdout: ${stdout.slice(0, 500)}`);
if (stderr) api.logger.warn(`memos-local: rebuild stderr: ${stderr.slice(0, 500)}`);

if (rebuildResult.status === 0) {
Object.keys(localRequire.cache)
.filter(k => k.includes("better-sqlite3") || k.includes("better_sqlite3"))
.forEach(k => delete localRequire.cache[k]);
sqliteReady = trySqliteLoad();
if (sqliteReady) {
api.logger.info("memos-local: better-sqlite3 auto-rebuild succeeded!");
} else {
api.logger.warn("memos-local: rebuild exited 0 but module still not loadable from plugin dir");
}
} else {
api.logger.warn(`memos-local: rebuild exited with code ${rebuildResult.status}`);
}
} catch (rebuildErr) {
api.logger.warn(`memos-local: auto-rebuild error: ${rebuildErr}`);
}

if (!sqliteReady) {
const nodeVer = process.version;
const nodeMajor = parseInt(process.versions?.node?.split(".")[0] ?? "0", 10);
const isNode25Plus = nodeMajor >= 25;
const lines = [
"",
"╔══════════════════════════════════════════════════════════════╗",
"║ MemOS Local Memory — better-sqlite3 native module missing ║",
"╠══════════════════════════════════════════════════════════════╣",
"║ ║",
"║ Auto-rebuild failed (Node " + nodeVer + "). Run manually: ║",
"║ ║",
`║ cd ${pluginDir}`,
"║ npm rebuild better-sqlite3 ║",
"║ openclaw gateway stop && openclaw gateway start ║",
"║ ║",
"║ If rebuild fails, install build tools first: ║",
"║ macOS: xcode-select --install ║",
"║ Linux: sudo apt install build-essential python3 ║",
];
if (isNode25Plus) {
lines.push("║ ║");
lines.push("║ Node 25+ has no prebuild: build tools required, or use ║");
lines.push("║ Node LTS (20/22): nvm install 22 && nvm use 22 ║");
}
lines.push("║ ║");
lines.push("╚══════════════════════════════════════════════════════════════╝");
lines.push("");
api.logger.warn(lines.join("\n"));
throw new Error(
`better-sqlite3 native module not found (Node ${nodeVer}). Auto-rebuild failed. Fix: install build tools, then cd ${pluginDir} && npm rebuild better-sqlite3. Or use Node LTS (20/22).`
);
}
}

const pluginDir = moduleDir; // alias — replaces removed detectPluginDir(moduleDir)
let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
const stateDir = process.env.OPENCLAW_STATE_DIR || api.resolvePath("~/.openclaw");

Expand Down
24 changes: 22 additions & 2 deletions apps/memos-local-openclaw/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}
},
"requirements": {
"node": ">=18.0.0",
"node": ">=22.0.0",
"openclaw": ">=2026.2.0"
},
"setup": {
Expand All @@ -33,5 +33,25 @@
"If better-sqlite3 fails to build, ensure you have C++ build tools: xcode-select --install (macOS) or build-essential (Linux)"
]
},
"extensions": ["./index.ts"]
"extensions": [
"./index.ts"
],
"contracts": {
"tools": [
"memory_search",
"memory_get",
"memory_timeline",
"memory_viewer",
"task_summary",
"skill_search",
"skill_install",
"skill_upgrade",
"memory_share",
"memory_unshare",
"skill_publish",
"skill_unpublish",
"network_skill_pull",
"network_team_info"
]
}
}
23 changes: 19 additions & 4 deletions apps/memos-local-openclaw/src/embedding/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { embedMistral } from "./providers/mistral";
import { embedLocal } from "./local";
import { modelHealth } from "../ingest/providers";

type EmbeddingInputKind = "document" | "query";

export class Embedder {
constructor(
private cfg: EmbeddingConfig | undefined,
Expand Down Expand Up @@ -57,13 +59,17 @@ export class Embedder {
if (this.provider === "cohere" && this.cfg) {
return embedCohereQuery(text, this.cfg, this.log);
}
const vecs = await this.embedBatch([text]);
const vecs = await this.embedBatch([text], "query");
return vecs[0];
}

private async embedBatch(texts: string[]): Promise<number[][]> {
private async embedBatch(
texts: string[],
inputKind: EmbeddingInputKind = "document",
): Promise<number[][]> {
const provider = this.provider;
const cfg = this.cfg;
const inputType = this.resolveInputType(inputKind);

const modelInfo = `${provider}/${cfg?.model ?? "default"}`;
try {
Expand All @@ -75,7 +81,9 @@ export class Embedder {
case "zhipu":
case "siliconflow":
case "bailian":
result = await embedOpenAI(texts, cfg!, this.log); break;
result = await embedOpenAI(texts, cfg!, this.log, inputType); break;
case "openclaw":
result = await this.embedOpenClaw(texts, inputType); break;
case "gemini":
result = await embedGemini(texts, cfg!, this.log); break;
case "cohere":
Expand All @@ -100,7 +108,13 @@ export class Embedder {
}
}

private async embedOpenClaw(texts: string[]): Promise<number[][]> {
private resolveInputType(inputKind: EmbeddingInputKind): string | undefined {
if (!this.cfg) return undefined;
if (inputKind === "query") return this.cfg.queryInputType ?? this.cfg.inputType;
return this.cfg.documentInputType ?? this.cfg.inputType;
}

private async embedOpenClaw(texts: string[], inputType?: string): Promise<number[][]> {
if (!this.openclawAPI) {
throw new Error(
"OpenClaw API not available. Ensure sharing.capabilities.hostEmbedding is enabled in config."
Expand All @@ -111,6 +125,7 @@ export class Embedder {
const response = await this.openclawAPI.embed({
texts,
model: this.cfg?.model,
inputType,
});

return response.embeddings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export async function embedGemini(
cfg: EmbeddingConfig,
log: Logger,
): Promise<number[][]> {
const model = cfg.model ?? "text-embedding-004";
const model = cfg.model ?? "gemini-embedding-001";
const endpoint =
cfg.endpoint ??
`https://generativelanguage.googleapis.com/v1beta/models/${model}:batchEmbedContents`;
Expand Down
7 changes: 6 additions & 1 deletion apps/memos-local-openclaw/src/embedding/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export async function embedOpenAI(
texts: string[],
cfg: EmbeddingConfig,
log: Logger,
inputType?: string,
): Promise<number[][]> {
const endpoint = normalizeEmbeddingEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/embeddings");
const model = cfg.model ?? "text-embedding-3-small";
Expand All @@ -16,7 +17,11 @@ export async function embedOpenAI(
const resp = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({ input: texts, model }),
body: JSON.stringify({
input: texts,
model,
...(inputType ? { input_type: inputType } : {}),
}),
signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
});

Expand Down
7 changes: 6 additions & 1 deletion apps/memos-local-openclaw/src/openclaw-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Logger, OpenClawAPI } from "./types";
export interface OpenClawEmbedRequest {
texts: string[];
model?: string;
inputType?: string;
}

export interface OpenClawEmbedResponse {
Expand Down Expand Up @@ -98,7 +99,11 @@ export class OpenClawAPIClient implements OpenClawAPI {
const resp = await fetch(endpoint, {
method: "POST",
headers: buildHeaders(provider),
body: JSON.stringify({ input: request.texts, model }),
body: JSON.stringify({
input: request.texts,
model,
...(request.inputType ? { input_type: request.inputType } : {}),
}),
signal: AbortSignal.timeout(30_000),
});

Expand Down
35 changes: 34 additions & 1 deletion apps/memos-local-openclaw/src/storage/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3011,6 +3011,39 @@ export class SqliteStore {
return result;
}

// ─── Export ───

exportAll(): { memories: unknown[]; tasks: unknown[]; skills: unknown[] } {
const memories = this.db.prepare(
"SELECT * FROM chunks ORDER BY created_at ASC",
).all();

const tasks = this.db.prepare(
"SELECT * FROM tasks ORDER BY started_at ASC",
).all();

const skills = this.db.prepare(
"SELECT id, name, description, content, version, status, visibility, owner, created_at, updated_at FROM skills ORDER BY created_at ASC",
).all();

return { memories, tasks, skills };
}

exportMemoriesAsCsv(): string {
const rows = this.db.prepare(
"SELECT id, session_key, role, summary, content, created_at FROM chunks ORDER BY created_at ASC",
).all() as Array<{ id: string; session_key: string; role: string; summary: string; content: string; created_at: number }>;

const escape = (v: string) => `"${String(v ?? "").replace(/"/g, '""')}"`;
const header = ["id", "session_key", "role", "summary", "content", "created_at"].join(",");
const lines = rows.map((r) =>
[r.id, r.session_key, r.role, r.summary, r.content, new Date(r.created_at).toISOString()]
.map(escape)
.join(","),
);
return [header, ...lines].join("\n");
}

close(): void {
this.db.close();
}
Expand All @@ -3031,7 +3064,7 @@ function sanitizeFtsQuery(raw: string): string {
.filter((t) => t.length > 1)
.filter((t) => !FTS_RESERVED.has(t.toUpperCase()));

return tokens.join(" ");
return tokens.map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
}

const FTS_RESERVED = new Set(["AND", "OR", "NOT", "NEAR"]);
Expand Down
16 changes: 14 additions & 2 deletions apps/memos-local-openclaw/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ export interface EmbeddingConfig extends ProviderConfig {
batchSize?: number;
dimensions?: number;
retry?: number;
inputType?: string;
queryInputType?: string;
documentInputType?: string;
}

// ─── Skill ───
Expand Down Expand Up @@ -419,8 +422,17 @@ export interface PluginContext {
}

export interface OpenClawAPI {
embed(request: { texts: string[]; model?: string }): Promise<{ embeddings: number[][]; dimensions: number }>;
complete(request: { prompt: string; maxTokens?: number; temperature?: number; model?: string }): Promise<{ text: string }>;
embed(request: {
texts: string[];
model?: string;
inputType?: string;
}): Promise<{ embeddings: number[][]; dimensions: number }>;
complete(request: {
prompt: string;
maxTokens?: number;
temperature?: number;
model?: string;
}): Promise<{ text: string }>;
}

export interface Logger {
Expand Down
Loading