Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ After installing, uninstalling, or changing config, restart the OpenClaw gateway
| `autoExperience` | No | Auto-extract procedural experiences via LLM; default `true`. |
| `experienceRecall` | No | Include experiences in recall results; default `true`. |
| `inferOnAdd` | No | Use PowerMem intelligent extraction when adding; default `true`. |
| `pluginLlmModel` | No | Optional `provider/model` for plugin-side LLM only (WAL capture, auto-experience). Use when `agents.defaults.model` is a router (e.g. `auto-router/auto`). Must match `models.providers`. Env: `MEMORY_POWERMEM_PLUGIN_LLM_MODEL`. If unset with a router primary: env → **first non-router key in `agents.defaults.models`** → first model under `models.providers`. |
| `importMarkdownOnStart` | No | One-time import of existing OpenClaw markdown memories on startup; default `false`. |
| `importMarkdownPaths` | No | Markdown files/directories to import. Defaults to `memory/`, `MEMORY.md`, and `USER.md`; relative paths resolve from the OpenClaw workspace. |
| `importMarkdownMaxFileBytes` | No | Max size for a single markdown file; default `10485760` (10 MiB). Larger files are marked `skipped_too_large`. |
Expand Down
9 changes: 6 additions & 3 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,16 @@ openclaw ltm search "咖啡"
| `requestConfig` | 否 | HTTP v2 专用:按请求透传 `config`(如 `memory_db`)。 |
| `envFile` | 否 | CLI:PowerMem `.env`;插件默认约定 `~/.openclaw/powermem/powermem.env`。 |
| `pmemPath` | 否 | CLI:`bundled`(默认)、`auto` 或 `pmem` 的路径/命令。 |
| `userId` | 否 | 用于多用户隔离。未填或为 `"auto"` 时自动生成并保存到 `<stateDir>/powermem/identity.json`。 |
| `agentId` | 否 | 用于多 Agent 隔离。未填或为 `"auto"` 时自动生成并保存到 `<stateDir>/powermem/identity.json`。 |
| `userId` | 否 | 多用户隔离。支持占位符如 `"${OPENCLAW_USER_NAME}"`(或任意 `${环境变量名}`):全部变量存在且非空则展开;否则与未填/`"auto"` 一样,回退到 `identity.json` 中已有值或生成新 UUID。 |
| `agentId` | 否 | 多 Agent 隔离。设为 **`"auto"`** 时,从 OpenClaw 配置里的 `agents.list[].id` 同步到 `<stateDir>/powermem/agent-identities.json`(每条 OpenClaw agent 对应一条映射,PowerMem 的 `agentId` 等于该条目的 `id`,如 `main`、`researcher`);`identity.json` 中的默认 `agentId` 取列表中的第一个。未填或非 `auto` 时行为与原先一致(单条默认 id)。 |
| `openclawConfigPath` | 否 | 仅在 `agentId` 为 `"auto"` 时使用:要读取的 OpenClaw JSON 路径,默认 `<stateDir>/openclaw.json`。 |
| `agentListSyncIntervalMs` | 否 | 仅在 `agentId` 为 `"auto"` 时:定时重新读取上述 JSON 并合并**新增**的 agent 到 `agent-identities.json`(毫秒)。`0` 表示不在运行时轮询(仅启动时同步一次)。省略时默认 `60000`(60 秒)。 |
| `autoCapture` | 否 | 会话结束后是否自动把对话交给 PowerMem 抽取记忆,默认 `true`。 |
| `autoRecall` | 否 | 会话开始前是否自动注入相关记忆,默认 `true`。 |
| `autoExperience` | 否 | LLM 自动提炼经验,默认 `true`。 |
| `experienceRecall` | 否 | 召回结果是否包含经验,默认 `true`。 |
| `inferOnAdd` | 否 | 写入时是否用 PowerMem 智能抽取,默认 `true`。 |
| `pluginLlmModel` | 否 | 可选,仅用于插件内 LLM(WAL、自动经验)。当 `agents.defaults.model` 为路由占位(如 `auto-router/auto`)时填写 `provider/model`,且须与 `models.providers` 一致。也可设环境变量 `MEMORY_POWERMEM_PLUGIN_LLM_MODEL`;未配置时依次尝试:**env** → **`agents.defaults.models` 里第一个非 `auto-router` 的键** → **`models.providers` 中第一个模型**。 |
| `importMarkdownOnStart` | 否 | 启动时一次性导入已有 OpenClaw markdown 记忆,默认 `false`。 |
| `importMarkdownPaths` | 否 | 要导入的 markdown 文件或目录。默认扫描 `memory/`、`MEMORY.md`、`USER.md`;相对路径基于 OpenClaw workspace。 |
| `importMarkdownMaxFileBytes` | 否 | 单个 markdown 文件最大大小,默认 `10485760`(10 MiB);超出的文件标记为 `skipped_too_large`。 |
Expand Down Expand Up @@ -351,7 +354,7 @@ openclaw ltm search "咖啡"
- `openclaw ltm import-md [paths...] [--force] [--dry-run] [--delay-ms n] [--max-file-bytes n] [--max-files n] [--max-chunks n]` — 导入已有 markdown 记忆;不传路径时扫描 `memory/`、`MEMORY.md`、`USER.md`
- `openclaw ltm import-md-status [paths...] [--json]` — 查看每个 markdown 文件的导入状态:已导入、已变更、跳过、失败或未导入

**身份文件(可选):** 若插件配置将 `userId` / `agentId` 设为 `auto`(或未填写),稳定 ID 会保存在 `<stateDir>/powermem/identity.json`(默认值)与 `<stateDir>/powermem/agent-identities.json`(按 OpenClaw agent key)。若在 `openclaw.json` 中显式设置了 `userId` 或 `agentId`,运行时将优先使用该配置,覆盖文件中的值
**身份文件(可选):** `userId` 可用 `${VAR}` 从环境变量取值;失败时回退到文件或自动生成。`agentId` 为 **`auto`** 时,会按 `agents.list` 维护 `agent-identities.json`(并可选按 `agentListSyncIntervalMs` 轮询 `openclawConfigPath` 以发现新 agent)。其它情况下:`userId` / `agentId` `auto`(或未填)时,稳定 ID 写在 `<stateDir>/powermem/identity.json`,按 agent key 的映射写在 `agent-identities.json`。若在 `openclaw.json` 插件配置里显式设置了非 `auto` 的 `userId` 或 `agentId`,运行时将优先使用该配置,覆盖文件中的对应逻辑(`userId` 仍以环境展开结果为准)

- `openclaw ltm identity show [--json]` — 打印路径及 `identity.json` 中存储的 `userId` / `agentId`。
- `openclaw ltm identity set --user-id <id>` / `--agent-id <id>` — 设置其一或两者(未指定的字段保留已有值或自动生成)。
Expand Down
25 changes: 23 additions & 2 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,25 @@
"label": "User ID",
"placeholder": "openclaw-user",
"advanced": true,
"help": "PowerMem user_id for memory isolation (optional)"
"help": "PowerMem user_id (optional). Use ${ENV_NAME} to read from the process environment; if any referenced variable is unset, falls back to identity.json / a new UUID. \"auto\" or omit = generated stable id."
},
"agentId": {
"label": "Agent ID",
"placeholder": "openclaw-agent",
"advanced": true,
"help": "PowerMem agent_id for memory isolation (optional)"
"help": "PowerMem agent_id (optional). Set to \"auto\" to mirror OpenClaw `agents.list[].id` into agent-identities.json (PowerMem agentId = that id); default row in identity.json uses the first listed agent."
},
"openclawConfigPath": {
"label": "OpenClaw config path",
"placeholder": "",
"advanced": true,
"help": "When agentId is auto: JSON file to read for `agents.list` (default: <stateDir>/openclaw.json). Used for startup + periodic sync."
},
"agentListSyncIntervalMs": {
"label": "Agent list sync interval (ms)",
"placeholder": "60000",
"advanced": true,
"help": "When agentId is auto: re-read the OpenClaw config file on this interval and merge new agents into agent-identities.json. 0 = no polling (sync once at startup). Omit for 60000."
},
"recallLimit": {
"label": "Recall limit",
Expand Down Expand Up @@ -211,6 +223,12 @@
"advanced": true,
"help": "Optional local vector fallback for dual-write (OpenAI/Ollama compatible embeddings + sqlite-vec)."
},
"pluginLlmModel": {
"label": "Plugin LLM model (provider/model)",
"advanced": true,
"placeholder": "e.g. glm-5_1",
"help": "Optional. For plugin-only LLM calls when agents.defaults.model is a router (e.g. auto-router/auto). Must match models.providers. Fallback order: env MEMORY_POWERMEM_PLUGIN_LLM_MODEL, then first non-router key in agents.defaults.models, then first model in models.providers."
},
"useOpenClawModel": {
"label": "Use OpenClaw LLM for PowerMem",
"advanced": true,
Expand All @@ -231,6 +249,8 @@
"pmemPath": { "type": "string" },
"userId": { "type": "string" },
"agentId": { "type": "string" },
"openclawConfigPath": { "type": "string" },
"agentListSyncIntervalMs": { "type": "number" },
"recallLimit": { "type": "number" },
"recallScoreThreshold": { "type": "number" },
"walCapture": { "type": "boolean" },
Expand Down Expand Up @@ -276,6 +296,7 @@
}
}
},
"pluginLlmModel": { "type": "string" },
"useOpenClawModel": { "type": "boolean" }
},
"required": []
Expand Down
80 changes: 80 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ function resolveEnvVars(value: string): string {
});
}

/**
* Replace `${VAR}` with `process.env[VAR]`. If any referenced variable is
* missing or empty, returns `undefined` (caller should fall back to file / UUID).
* Strings without placeholders are returned trimmed as-is.
*/
export function expandOptionalEnvPlaceholders(input: string | undefined): string | undefined {
if (input === undefined) return undefined;
const trimmed = input.trim();
if (!trimmed) return undefined;
const re = /\$\{([^}]+)\}/g;
if (!re.test(trimmed)) {
return trimmed;
}
re.lastIndex = 0;
let out = "";
let lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = re.exec(trimmed)) !== null) {
const name = m[1].trim();
const val = process.env[name];
if (val === undefined || val === "") {
return undefined;
}
out += trimmed.slice(lastIndex, m.index) + val;
lastIndex = m.index + m[0].length;
}
out += trimmed.slice(lastIndex);
const merged = out.trim();
return merged.length > 0 ? merged : undefined;
}

export type PowerMemMode = "http" | "cli";
export type PowerMemHttpApiVersion = "v1" | "v2";
export type DualWritePriority = "remote" | "local";
Expand All @@ -51,8 +82,23 @@ export type PowerMemConfig = {
* (overrides the same keys from an optional .env file). SQLite defaults live under the OpenClaw state dir.
*/
useOpenClawModel?: boolean;
/**
* Optional `provider/model` for plugin-only LLM calls (WAL capture, auto-experience).
* Use when `agents.defaults.model.primary` is a router placeholder (e.g. `auto-router/auto`) that OpenClaw resolves internally but this plugin cannot load via `models.providers`.
*/
pluginLlmModel?: string;
userId?: string;
agentId?: string;
/**
* When `agentId` is `auto`, poll this path (JSON) for `agents.list` and merge
* into `agent-identities.json`. Default: `<stateDir>/openclaw.json`.
*/
openclawConfigPath?: string;
/**
* Interval (ms) to re-read OpenClaw config for new agents when `agentId` is `auto`.
* `0` disables polling (sync once at startup). Default when omitted: `60000`.
*/
agentListSyncIntervalMs?: number;
/** Max memories to return in recall / inject in auto-recall. Default 5. */
recallLimit?: number;
/** Min score (0–1) for recall; memories below are filtered. Default 0. */
Expand Down Expand Up @@ -114,8 +160,11 @@ const ALLOWED_KEYS = [
"envFile",
"pmemPath",
"useOpenClawModel",
"pluginLlmModel",
"userId",
"agentId",
"openclawConfigPath",
"agentListSyncIntervalMs",
"recallLimit",
"recallScoreThreshold",
"walCapture",
Expand Down Expand Up @@ -272,6 +321,10 @@ export const powerMemConfigSchema = {
envFile,
pmemPath,
useOpenClawModel: cfg.useOpenClawModel !== false,
pluginLlmModel:
typeof cfg.pluginLlmModel === "string" && cfg.pluginLlmModel.trim()
? cfg.pluginLlmModel.trim()
: undefined,
userId:
typeof cfg.userId === "string" && cfg.userId.trim()
? cfg.userId.trim()
Expand Down Expand Up @@ -307,6 +360,14 @@ export const powerMemConfigSchema = {
typeof cfg.localAgentId === "string" && cfg.localAgentId.trim()
? cfg.localAgentId.trim()
: undefined,
openclawConfigPath:
typeof cfg.openclawConfigPath === "string" && cfg.openclawConfigPath.trim()
? cfg.openclawConfigPath.trim()
: undefined,
agentListSyncIntervalMs: toOptionalNonNegativeInt(
cfg.agentListSyncIntervalMs,
86400000,
),
syncOnResume: cfg.syncOnResume !== false,
syncBatchSize,
syncMinIntervalMs,
Expand Down Expand Up @@ -377,6 +438,25 @@ function toOptionalPositiveInt(v: unknown, min: number, max: number): number | u
return undefined;
}

/** Optional non-negative int (e.g. sync interval); clamped to `max`. */
function toOptionalNonNegativeInt(v: unknown, max: number): number | undefined {
if (v === undefined || v === null || v === "") {
return undefined;
}
if (typeof v === "number" && Number.isFinite(v)) {
const n = Math.floor(v);
return n >= 0 ? Math.min(max, n) : undefined;
}
if (typeof v === "string" && v.trim() !== "") {
const n = Number(v);
if (Number.isFinite(n)) {
const floored = Math.floor(n);
return floored >= 0 ? Math.min(max, floored) : undefined;
}
}
return undefined;
}

function parseHeaderMap(value: unknown): Record<string, string> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
Expand Down
Loading
Loading