memorylake-openclaw 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,134 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import type { PluginContext } from "../plugin-context";
4
- import type { MemoryLakeConfig, UploadFn } from "../types";
5
- import { getProvider } from "../provider";
6
- import { buildSearchOptions } from "../utils/builders";
7
- import { readJson5ConfigFile } from "../utils/config-parser";
8
-
9
- export function registerCli(pctx: PluginContext, cfg: MemoryLakeConfig): void {
10
- const { api, resolveConfig } = pctx;
11
- const provider = getProvider(cfg);
12
-
13
- api.registerCli(
14
- ({ program }) => {
15
- const memorylake = program
16
- .command("memorylake")
17
- .description("MemoryLake memory plugin commands");
18
-
19
- memorylake
20
- .command("search")
21
- .description("Search memories in MemoryLake")
22
- .argument("<query>", "Search query")
23
- .option("--limit <n>", "Max results", String(cfg.topK))
24
- .action(async (query: string, opts: { limit: string }) => {
25
- try {
26
- const limit = parseInt(opts.limit, 10);
27
- const results = await provider.search(
28
- query,
29
- buildSearchOptions(cfg, undefined, limit),
30
- );
31
-
32
- if (!results.length) {
33
- console.log("No memories found.");
34
- return;
35
- }
36
-
37
- const output = results.map((r) => ({
38
- id: r.id,
39
- content: r.content,
40
- user_id: r.user_id,
41
- created_at: r.created_at,
42
- }));
43
- console.log(JSON.stringify(output, null, 2));
44
- } catch (err) {
45
- console.error(`Search failed: ${String(err)}`);
46
- }
47
- });
48
-
49
- memorylake
50
- .command("upload")
51
- .description("Upload files or directories to MemoryLake")
52
- .argument("<path>", "File or directory path to upload")
53
- .option("--agent <id>", "Agent ID (resolves workspace and per-agent projectId)")
54
- .option("--project-id <id>", "Override project ID (takes precedence over --agent)")
55
- .action(async (targetPath: string, opts: { agent?: string; projectId?: string }) => {
56
- // Resolve effective config: --project-id > agent workspace config > global config
57
- let effectiveCfg = cfg;
58
- if (opts.agent) {
59
- try {
60
- const openclawPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
61
- const openclaw = readJson5ConfigFile(openclawPath) as any;
62
- const agents = openclaw?.agents;
63
- const agentEntry = agents?.list?.find((a: any) => a.id === opts.agent);
64
- const workspace = agentEntry?.workspace || agents?.defaults?.workspace;
65
- if (workspace) {
66
- effectiveCfg = resolveConfig({ workspaceDir: workspace });
67
- } else {
68
- console.warn(`Warning: no workspace found for agent "${opts.agent}", using global config.`);
69
- }
70
- } catch (err) {
71
- console.warn(`Warning: failed to resolve agent config: ${String(err)}, using global config.`);
72
- }
73
- }
74
- const effectiveProjectId = opts.projectId || effectiveCfg.projectId;
75
- if (!effectiveProjectId) {
76
- console.error("No project ID configured. Use --project-id or set up agent/workspace config.");
77
- return;
78
- }
79
- if (!effectiveCfg.host || !effectiveCfg.apiKey) {
80
- console.error("Missing host or apiKey in config. Check your MemoryLake configuration.");
81
- return;
82
- }
83
-
84
- // Lazy import upload.mjs (use uploadAuto to support archives)
85
- let uploadFn: UploadFn;
86
- try {
87
- const uploadModule = await import(
88
- /* webpackIgnore: true */
89
- new URL("../../skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
90
- );
91
- uploadFn = uploadModule.uploadAuto;
92
- } catch (err) {
93
- console.error(`Failed to load upload module: ${String(err)}`);
94
- return;
95
- }
96
-
97
- const absPath = path.resolve(targetPath);
98
-
99
- try {
100
- await uploadFn({
101
- host: effectiveCfg.host,
102
- apiKey: effectiveCfg.apiKey,
103
- projectId: effectiveProjectId,
104
- filePath: absPath,
105
- fileName: path.basename(absPath),
106
- });
107
- } catch (err) {
108
- console.error(`Upload failed: ${String(err)}`);
109
- }
110
- });
111
-
112
- memorylake
113
- .command("stats")
114
- .description("Show memory statistics from MemoryLake")
115
- .action(async () => {
116
- try {
117
- const memories = await provider.getAll({
118
- user_id: cfg.userId,
119
- });
120
- console.log(`User: ${cfg.userId}`);
121
- console.log(
122
- `Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`,
123
- );
124
- console.log(
125
- `Auto-recall: ${cfg.autoRecall}, Auto-capture: ${cfg.autoCapture}`,
126
- );
127
- } catch (err) {
128
- console.error(`Stats failed: ${String(err)}`);
129
- }
130
- });
131
- },
132
- { commands: ["memorylake"] },
133
- );
134
- }
package/lib/config.ts DELETED
@@ -1,105 +0,0 @@
1
- import type { MemoryLakeConfig } from "./types";
2
- import { DEFAULT_USER_ID } from "./types";
3
-
4
- // ============================================================================
5
- // Config Schema
6
- // ============================================================================
7
-
8
- export const ALLOWED_KEYS = [
9
- "host",
10
- "apiKey",
11
- "projectId",
12
- "userId",
13
- "autoCapture",
14
- "autoRecall",
15
- "autoUpload",
16
- "searchThreshold",
17
- "topK",
18
- "rerank",
19
- "webSearchIncludeDomains",
20
- "webSearchExcludeDomains",
21
- "webSearchCountry",
22
- "webSearchTimezone",
23
- ];
24
-
25
- function assertAllowedKeys(
26
- value: Record<string, unknown>,
27
- allowed: string[],
28
- label: string,
29
- ) {
30
- const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
31
- if (unknown.length === 0) return;
32
- throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
33
- }
34
-
35
- function parseOptionalStringArray(
36
- value: unknown,
37
- label: string,
38
- ): string[] | undefined {
39
- if (value == null) return undefined;
40
- if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
41
- throw new Error(`${label} must be an array of strings`);
42
- }
43
- return value;
44
- }
45
-
46
- function parseOptionalString(
47
- value: unknown,
48
- label: string,
49
- ): string | undefined {
50
- if (value == null) return undefined;
51
- if (typeof value !== "string") {
52
- throw new Error(`${label} must be a string`);
53
- }
54
- return value;
55
- }
56
-
57
- export const memoryLakeConfigSchema = {
58
- parse(value: unknown): MemoryLakeConfig {
59
- if (!value || typeof value !== "object" || Array.isArray(value)) {
60
- throw new Error("memorylake-openclaw config required");
61
- }
62
- const cfg = value as Record<string, unknown>;
63
- assertAllowedKeys(cfg, ALLOWED_KEYS, "memorylake-openclaw config");
64
-
65
- if (typeof cfg.apiKey !== "string" || !cfg.apiKey) {
66
- throw new Error("apiKey is required");
67
- }
68
- if (typeof cfg.projectId !== "string" || !cfg.projectId) {
69
- throw new Error("projectId is required");
70
- }
71
-
72
- return {
73
- host:
74
- typeof cfg.host === "string" && cfg.host
75
- ? cfg.host
76
- : "https://app.memorylake.ai",
77
- apiKey: cfg.apiKey as string,
78
- projectId: cfg.projectId as string,
79
- userId: DEFAULT_USER_ID,
80
- autoCapture: cfg.autoCapture !== false,
81
- autoRecall: cfg.autoRecall !== false,
82
- autoUpload: cfg.autoUpload !== false,
83
- searchThreshold:
84
- typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.3,
85
- topK: typeof cfg.topK === "number" ? cfg.topK : 5,
86
- rerank: cfg.rerank !== false,
87
- webSearchIncludeDomains: parseOptionalStringArray(
88
- cfg.webSearchIncludeDomains,
89
- "webSearchIncludeDomains",
90
- ),
91
- webSearchExcludeDomains: parseOptionalStringArray(
92
- cfg.webSearchExcludeDomains,
93
- "webSearchExcludeDomains",
94
- ),
95
- webSearchCountry: parseOptionalString(
96
- cfg.webSearchCountry,
97
- "webSearchCountry",
98
- ),
99
- webSearchTimezone: parseOptionalString(
100
- cfg.webSearchTimezone,
101
- "webSearchTimezone",
102
- ),
103
- };
104
- },
105
- };
@@ -1,155 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
-
5
- export type CoreConfig = {
6
- session?: {
7
- store?: string;
8
- };
9
- [key: string]: unknown;
10
- };
11
-
12
- type CoreAgentDeps = {
13
- resolveAgentDir: (cfg: CoreConfig, agentId: string) => string;
14
- resolveAgentWorkspaceDir: (cfg: CoreConfig, agentId: string) => string;
15
- resolveAgentIdentity: (
16
- cfg: CoreConfig,
17
- agentId: string,
18
- ) => { name?: string | null } | null | undefined;
19
- resolveThinkingDefault: (params: {
20
- cfg: CoreConfig;
21
- provider?: string;
22
- model?: string;
23
- }) => string;
24
- runEmbeddedPiAgent: (params: {
25
- sessionId: string;
26
- sessionKey?: string;
27
- messageProvider?: string;
28
- sessionFile: string;
29
- workspaceDir: string;
30
- config?: CoreConfig;
31
- prompt: string;
32
- provider?: string;
33
- model?: string;
34
- thinkLevel?: string;
35
- verboseLevel?: string;
36
- timeoutMs: number;
37
- runId: string;
38
- lane?: string;
39
- extraSystemPrompt?: string;
40
- agentDir?: string;
41
- }) => Promise<{
42
- payloads?: Array<{ text?: string; isError?: boolean }>;
43
- meta?: { aborted?: boolean };
44
- }>;
45
- resolveAgentTimeoutMs: (opts: { cfg: CoreConfig }) => number;
46
- ensureAgentWorkspace: (params?: { dir: string }) => Promise<void>;
47
- resolveStorePath: (store?: string, opts?: { agentId?: string }) => string;
48
- loadSessionStore: (storePath: string) => Record<string, unknown>;
49
- saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
50
- resolveSessionFilePath: (
51
- sessionId: string,
52
- entry: unknown,
53
- opts?: { agentId?: string },
54
- ) => string;
55
- DEFAULT_MODEL: string;
56
- DEFAULT_PROVIDER: string;
57
- };
58
-
59
- let coreRootCache: string | null = null;
60
- let coreDepsPromise: Promise<CoreAgentDeps> | null = null;
61
-
62
- function findPackageRoot(startDir: string, name: string): string | null {
63
- let dir = startDir;
64
- for (;;) {
65
- const pkgPath = path.join(dir, "package.json");
66
- try {
67
- if (fs.existsSync(pkgPath)) {
68
- const raw = fs.readFileSync(pkgPath, "utf8");
69
- const pkg = JSON.parse(raw) as { name?: string };
70
- if (pkg.name === name) {
71
- return dir;
72
- }
73
- }
74
- } catch {
75
- // ignore parse errors and keep walking
76
- }
77
- const parent = path.dirname(dir);
78
- if (parent === dir) {
79
- return null;
80
- }
81
- dir = parent;
82
- }
83
- }
84
-
85
- function resolveOpenClawRoot(): string {
86
- if (coreRootCache) {
87
- return coreRootCache;
88
- }
89
- const override = process.env.OPENCLAW_ROOT?.trim();
90
- if (override) {
91
- coreRootCache = override;
92
- return override;
93
- }
94
-
95
- const candidates = new Set<string>();
96
- if (process.argv[1]) {
97
- candidates.add(path.dirname(process.argv[1]));
98
- }
99
- candidates.add(process.cwd());
100
- try {
101
- const urlPath = fileURLToPath(import.meta.url);
102
- candidates.add(path.dirname(urlPath));
103
- } catch {
104
- // ignore
105
- }
106
-
107
- for (const start of candidates) {
108
- for (const name of ["openclaw"]) {
109
- const found = findPackageRoot(start, name);
110
- if (found) {
111
- coreRootCache = found;
112
- return found;
113
- }
114
- }
115
- }
116
-
117
- throw new Error("Unable to resolve core root. Set OPENCLAW_ROOT to the package root.");
118
- }
119
-
120
- async function importCoreExtensionAPI(): Promise<{
121
- resolveAgentDir: CoreAgentDeps["resolveAgentDir"];
122
- resolveAgentWorkspaceDir: CoreAgentDeps["resolveAgentWorkspaceDir"];
123
- DEFAULT_MODEL: string;
124
- DEFAULT_PROVIDER: string;
125
- resolveAgentIdentity: CoreAgentDeps["resolveAgentIdentity"];
126
- resolveThinkingDefault: CoreAgentDeps["resolveThinkingDefault"];
127
- runEmbeddedPiAgent: CoreAgentDeps["runEmbeddedPiAgent"];
128
- resolveAgentTimeoutMs: CoreAgentDeps["resolveAgentTimeoutMs"];
129
- ensureAgentWorkspace: CoreAgentDeps["ensureAgentWorkspace"];
130
- resolveStorePath: CoreAgentDeps["resolveStorePath"];
131
- loadSessionStore: CoreAgentDeps["loadSessionStore"];
132
- saveSessionStore: CoreAgentDeps["saveSessionStore"];
133
- resolveSessionFilePath: CoreAgentDeps["resolveSessionFilePath"];
134
- }> {
135
- // Do not import any other module. You can't touch this or you will be fired.
136
- const distPath = path.join(resolveOpenClawRoot(), "dist", "extensionAPI.js");
137
- if (!fs.existsSync(distPath)) {
138
- throw new Error(
139
- `Missing core module at ${distPath}. Run \`pnpm build\` or install the official package.`,
140
- );
141
- }
142
- return await import(pathToFileURL(distPath).href);
143
- }
144
-
145
- export async function loadCoreAgentDeps(): Promise<CoreAgentDeps> {
146
- if (coreDepsPromise) {
147
- return coreDepsPromise;
148
- }
149
-
150
- coreDepsPromise = (async () => {
151
- return await importCoreExtensionAPI();
152
- })();
153
-
154
- return coreDepsPromise;
155
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * Parse the filename from a Content-Disposition header.
3
- * Handles both `filename="..."` and RFC 5987 `filename*=UTF-8''...` forms.
4
- * Returns null if not found or unparseable.
5
- */
6
- export function parseContentDispositionFilename(header?: string): string | null {
7
- if (!header) return null;
8
- // RFC 5987: filename*=UTF-8''encoded%20name.pdf (takes priority)
9
- const star = header.match(/filename\*\s*=\s*(?:UTF-8|utf-8)?''(.+?)(?:;|$)/i);
10
- if (star?.[1]) {
11
- try {
12
- return decodeURIComponent(star[1].trim());
13
- } catch (err) {
14
- console.warn(`memorylake-openclaw: failed to decode Content-Disposition filename* value "${star[1]}": ${String(err)}`);
15
- }
16
- }
17
- // Standard: filename="name.pdf" or filename=name.pdf
18
- const plain = header.match(/filename\s*=\s*"?([^";]+)"?/i);
19
- if (plain?.[1]) return plain[1].trim();
20
- return null;
21
- }
@@ -1,122 +0,0 @@
1
- import fsPromises from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import type { PluginContext } from "../plugin-context";
5
- import { loadCoreAgentDeps } from "../core-bridge";
6
-
7
- /**
8
- * Summarize recent session messages into a compact text block for the rewrite prompt.
9
- * Messages are unknown[] from the hook event — we extract role+content from each.
10
- */
11
- export function summarizeMessages(messages: unknown[], maxMessages = 10): string {
12
- if (!messages || messages.length === 0) return "";
13
- const recent = messages.slice(-maxMessages);
14
- return recent
15
- .map((m: any) => {
16
- const role = m?.role ?? "user";
17
- const content =
18
- typeof m?.content === "string"
19
- ? m.content
20
- : JSON.stringify(m?.content ?? "");
21
- return `[${role}]: ${content}`;
22
- })
23
- .join("\n");
24
- }
25
-
26
- /**
27
- * Resolve provider/model from config. Returns undefined for both if not found
28
- * (openclaw will use its own defaults).
29
- */
30
- export function resolveProviderModel(api: PluginContext["api"]): { provider: string | undefined; model: string | undefined } {
31
- const modelPrimary = (api.config as any)?.agents?.defaults?.model?.primary as string | undefined;
32
- if (modelPrimary) {
33
- const slashIdx = modelPrimary.indexOf("/");
34
- if (slashIdx >= 0) {
35
- return { provider: modelPrimary.slice(0, slashIdx), model: modelPrimary.slice(slashIdx + 1) };
36
- }
37
- return { provider: undefined, model: modelPrimary };
38
- }
39
- return { provider: undefined, model: undefined };
40
- }
41
-
42
- /**
43
- * Rewrite the user's prompt into a search-optimized query using
44
- * openclaw's runEmbeddedPiAgent, considering conversation history.
45
- *
46
- * Priority: api.runtime.agent.runEmbeddedPiAgent → loadCoreAgentDeps()
47
- */
48
- export async function rewriteQueryForSearch(
49
- api: PluginContext["api"],
50
- originalPrompt: string,
51
- messages: unknown[],
52
- ctx: { workspaceDir?: string },
53
- ): Promise<string> {
54
- if (!ctx.workspaceDir) {
55
- api.logger.warn("memorylake-openclaw: no workspaceDir, skipping query rewrite");
56
- return originalPrompt;
57
- }
58
-
59
- const conversationHistory = summarizeMessages(messages);
60
- const systemPrompt =
61
- "You are a search query optimizer. Extract the key search intent and produce a concise, search-optimized query. Output ONLY the rewritten query, nothing else. Preserve important entities, names, dates, and technical terms.";
62
- const userContent = conversationHistory
63
- ? `Conversation history:\n${conversationHistory}\n\nUser's latest message:\n${originalPrompt}`
64
- : originalPrompt;
65
- const fullPrompt = `${systemPrompt}\n\n${userContent}`;
66
-
67
- const { provider, model } = resolveProviderModel(api);
68
- api.logger.info(`memorylake-openclaw: rewriting query via runEmbeddedPiAgent (provider=${provider}, model=${model})`);
69
-
70
- let tempSessionFile: string | null = null;
71
- try {
72
- const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "memorylake-rewrite-"));
73
- tempSessionFile = path.join(tempDir, "session.jsonl");
74
-
75
- const nowMs = Date.now();
76
- const callParams = {
77
- sessionId: `memorylake-rewrite-${nowMs}`,
78
- sessionKey: `temp:memorylake-rewrite`,
79
- sessionFile: tempSessionFile,
80
- workspaceDir: ctx.workspaceDir,
81
- config: api.config,
82
- prompt: fullPrompt,
83
- provider,
84
- model,
85
- disableTools: true,
86
- timeoutMs: 15_000,
87
- runId: `memorylake-rewrite-${nowMs}`,
88
- lane: `memorylake-rewrite`,
89
- trigger: "memory",
90
- };
91
-
92
- // Priority 1: try api.runtime.agent.runEmbeddedPiAgent
93
- let runEmbeddedPiAgent: ((p: typeof callParams) => Promise<any>) | undefined =
94
- (api.runtime as any)?.agent?.runEmbeddedPiAgent;
95
-
96
- if (typeof runEmbeddedPiAgent !== "function") {
97
- api.logger.info("memorylake-openclaw: api.runtime.agent.runEmbeddedPiAgent not available, using loadCoreAgentDeps fallback");
98
- const deps = await loadCoreAgentDeps();
99
- runEmbeddedPiAgent = deps.runEmbeddedPiAgent;
100
- }
101
-
102
- const result = await runEmbeddedPiAgent(callParams);
103
-
104
- const rewritten = result?.payloads?.[0]?.text?.trim();
105
- if (rewritten && rewritten.length > 0) {
106
- api.logger.info(`memorylake-openclaw: rewritten query: "${rewritten}"`);
107
- return rewritten;
108
- }
109
- api.logger.warn("memorylake-openclaw: rewrite returned empty, using original");
110
- } catch (err) {
111
- api.logger.warn(`memorylake-openclaw: query rewrite failed, using original: ${String(err)}`);
112
- } finally {
113
- if (tempSessionFile) {
114
- try {
115
- await fsPromises.rm(path.dirname(tempSessionFile), { recursive: true, force: true });
116
- } catch (cleanupErr) {
117
- api.logger.warn(`memorylake-openclaw: temp session cleanup failed: ${String(cleanupErr)}`);
118
- }
119
- }
120
- }
121
- return originalPrompt;
122
- }
@@ -1,47 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- const UPLOADED_RECORD_FILE = "uploaded.json";
5
-
6
- export type UploadedRecord = Record<string, { mtimeMs: number }>;
7
-
8
- export function getUploadedRecord(workspaceDir: string): UploadedRecord {
9
- const filePath = path.join(workspaceDir, ".memorylake", UPLOADED_RECORD_FILE);
10
- try {
11
- if (fs.existsSync(filePath)) {
12
- const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
13
- return data && typeof data === "object" && !Array.isArray(data) ? data : {};
14
- }
15
- } catch {
16
- // ignore read errors
17
- }
18
- return {};
19
- }
20
-
21
- export function saveUploadedRecord(workspaceDir: string, record: UploadedRecord): void {
22
- const dirPath = path.join(workspaceDir, ".memorylake");
23
- if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
24
- fs.writeFileSync(
25
- path.join(dirPath, UPLOADED_RECORD_FILE),
26
- JSON.stringify(record, null, 2),
27
- );
28
- }
29
-
30
- export function needsUpload(record: UploadedRecord, filePath: string): fs.Stats | null {
31
- if (!fs.existsSync(filePath)) return null;
32
- const stat = fs.statSync(filePath);
33
- const prev = record[filePath];
34
- return (!prev || prev.mtimeMs !== stat.mtimeMs) ? stat : null;
35
- }
36
-
37
- export function extractInboundPaths(prompt: string): string[] {
38
- // Path must contain /media/inbound/ (or \media\inbound\)
39
- // Filename must end with .<ext>, ext = alphanumeric, 1-6 chars
40
- const sep = '[/\\\\]';
41
- const regex = new RegExp(
42
- `(?:[A-Za-z]:${sep}|/)\\S*?media${sep}inbound${sep}.+?\\.[a-zA-Z0-9]{1,6}(?=[^a-zA-Z0-9]|$)`,
43
- "g",
44
- );
45
- const matches = prompt.match(regex) || [];
46
- return [...new Set(matches)];
47
- }
@@ -1,111 +0,0 @@
1
- import type { PluginContext } from "../plugin-context";
2
- import { getProvider } from "../provider";
3
- import { buildAddOptions } from "../utils/builders";
4
- import { stripUserBody } from "../utils/strip-user-body";
5
-
6
- // Per-session high-water mark of the most recent message timestamp we've
7
- // already forwarded to the provider. Each agent_end fires with the full
8
- // session snapshot, so without this we'd re-send the entire history every
9
- // turn. Keyed by sessionId; lost across plugin restarts (the provider's
10
- // own dedupe logic handles that case).
11
- const sessionWatermarks = new Map<string, number>();
12
-
13
- function extractText(content: unknown): string {
14
- if (typeof content === "string") return content;
15
- if (!Array.isArray(content)) return "";
16
- let text = "";
17
- for (const block of content) {
18
- if (
19
- block &&
20
- typeof block === "object" &&
21
- "text" in block &&
22
- typeof (block as Record<string, unknown>).text === "string"
23
- ) {
24
- text += (text ? "\n" : "") + ((block as Record<string, unknown>).text as string);
25
- }
26
- }
27
- return text;
28
- }
29
-
30
- export function registerAutoCapture(pctx: PluginContext): void {
31
- const { api, resolveConfig } = pctx;
32
-
33
- api.on("agent_end", async (event, ctx) => {
34
- if ((ctx as any)?.trigger !== "user") {
35
- api.logger.info(`memorylake-openclaw: auto-capture skipped, trigger=${(ctx as any)?.trigger ?? "undefined"}`);
36
- return;
37
- }
38
- if (!event.success || !event.messages || event.messages.length === 0) {
39
- return;
40
- }
41
-
42
- // The plugin hook context types sessionId as optional, but the only path
43
- // that fires `agent_end` (pi-embedded-runner/run/attempt.ts) always
44
- // provides a non-empty string from RunEmbeddedPiAgentParams.sessionId.
45
- // If a future fire site or a runtime quirk produces an empty sessionId,
46
- // we'd lose watermark dedup and start re-sending the entire snapshot
47
- // every turn — bail out instead of silently degrading.
48
- const sessionId: string | undefined = (ctx as any)?.sessionId ?? undefined;
49
- if (!sessionId) {
50
- api.logger.warn("memorylake-openclaw: auto-capture skipped, sessionId missing from context");
51
- return;
52
- }
53
-
54
- // Resolve per-workspace config override
55
- const effectiveCfg = resolveConfig(ctx);
56
- const effectiveProvider = getProvider(effectiveCfg);
57
-
58
- const lastSent = sessionWatermarks.get(sessionId) ?? 0;
59
-
60
- try {
61
- // Walk the full snapshot, take only messages newer than our watermark
62
- // and only user / assistant roles (toolResult is internal plumbing).
63
- // Strip openclaw inbound-metadata wrappers from user messages; pass
64
- // assistant content through unchanged. Whether to extract facts from
65
- // assistant replies is the provider's call.
66
- const formattedMessages: Array<{ role: string; content: string }> = [];
67
- let maxTimestamp = lastSent;
68
-
69
- for (const msg of event.messages) {
70
- if (!msg || typeof msg !== "object") continue;
71
- const obj = msg as Record<string, unknown>;
72
- const role = obj.role;
73
- if (role !== "user" && role !== "assistant") continue;
74
-
75
- const ts = typeof obj.timestamp === "number" ? obj.timestamp : 0;
76
- if (ts <= lastSent) continue;
77
- if (ts > maxTimestamp) maxTimestamp = ts;
78
-
79
- const raw = extractText(obj.content);
80
- if (!raw) continue;
81
-
82
- const content = role === "user" ? stripUserBody(raw) : raw;
83
- if (!content) continue;
84
-
85
- formattedMessages.push({ role, content });
86
- }
87
-
88
- if (formattedMessages.length === 0) {
89
- return;
90
- }
91
-
92
- const addOpts = buildAddOptions(effectiveCfg, undefined, sessionId);
93
- const result = await effectiveProvider.add(formattedMessages, addOpts);
94
-
95
- // Advance the watermark only after a successful add — if the call
96
- // throws, we'll retry the same range on the next turn.
97
- if (maxTimestamp > lastSent) {
98
- sessionWatermarks.set(sessionId, maxTimestamp);
99
- }
100
-
101
- const capturedCount = result.results?.length ?? 0;
102
- if (capturedCount > 0) {
103
- api.logger.info(
104
- `memorylake-openclaw: auto-captured ${capturedCount} memories from ${formattedMessages.length} new message(s)`,
105
- );
106
- }
107
- } catch (err) {
108
- api.logger.warn(`memorylake-openclaw: capture failed: ${String(err)}`);
109
- }
110
- });
111
- }