@memoryrelay/plugin-memoryrelay-ai 0.17.1 → 0.18.0

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.
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * OpenClaw Memory Plugin - MemoryRelay
3
- * Version: 0.17.1
3
+ * Version: 0.18.0
4
4
  *
5
5
  * Long-term memory with vector search using MemoryRelay API.
6
6
  * Provides auto-recall and auto-capture via lifecycle hooks.
@@ -444,7 +444,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
444
444
  // Register Hooks (8 modules)
445
445
  // ========================================================================
446
446
 
447
- registerBeforeAgentStart(api, pluginConfig, isToolEnabled, defaultProject);
447
+ registerBeforeAgentStart(api, pluginConfig, client, isToolEnabled, defaultProject);
448
448
  registerBeforePromptBuild(api, pluginConfig, client, sessionResolver, localCache, syncDaemon);
449
449
  registerAgentEnd(api, pluginConfig, client, sessionResolver, localCache, syncDaemon);
450
450
  registerSessionLifecycle(api, pluginConfig, client, agentId, defaultProject, sessionResolver);
@@ -472,7 +472,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
472
472
  // ========================================================================
473
473
 
474
474
  api.logger.info?.(
475
- `memory-memoryrelay: plugin v0.17.1 loaded (${Object.values(TOOL_GROUPS).flat().length} tools, autoRecall: ${pluginConfig.autoRecall}, autoCapture: ${autoCaptureConfig.enabled ? autoCaptureConfig.tier : "off"}, debug: ${debugEnabled})`,
475
+ `memory-memoryrelay: plugin v0.18.0 loaded (${Object.values(TOOL_GROUPS).flat().length} tools, autoRecall: ${pluginConfig.autoRecall}, autoCapture: ${autoCaptureConfig.enabled ? autoCaptureConfig.tier : "off"}, debug: ${debugEnabled})`,
476
476
  );
477
477
 
478
478
  // ========================================================================
@@ -2,8 +2,8 @@
2
2
  "id": "plugin-memoryrelay-ai",
3
3
  "kind": "memory",
4
4
  "name": "MemoryRelay AI",
5
- "description": "MemoryRelay v0.17.1 - Long-term memory with pipeline architecture, 42 tools, 17 commands, V2 async, sessions, decisions, patterns & projects (api.memoryrelay.net)",
6
- "version": "0.17.1",
5
+ "description": "MemoryRelay v0.18.0 - Long-term memory with pipeline architecture, 42 tools, 17 commands, V2 async, sessions, decisions, patterns & projects (api.memoryrelay.net)",
6
+ "version": "0.18.0",
7
7
  "uiHints": {
8
8
  "apiKey": {
9
9
  "label": "MemoryRelay API Key",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoryrelay/plugin-memoryrelay-ai",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "OpenClaw memory plugin for MemoryRelay API - 42 tools, 17 commands, V2 async, sessions, decisions, patterns, projects & semantic search",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -42,7 +42,8 @@
42
42
  "openclaw": {
43
43
  "extensions": [
44
44
  "index.ts"
45
- ]
45
+ ],
46
+ "hooks": []
46
47
  },
47
48
  "files": [
48
49
  "index.ts",
@@ -153,7 +153,7 @@ export class MemoryRelayClient implements IMemoryRelayClient {
153
153
  headers: {
154
154
  "Content-Type": "application/json",
155
155
  Authorization: `Bearer ${this.apiKey}`,
156
- "User-Agent": "openclaw-memory-memoryrelay/0.17.1",
156
+ "User-Agent": "openclaw-memory-memoryrelay/0.18.0",
157
157
  },
158
158
  body: body ? JSON.stringify(body) : undefined,
159
159
  },
@@ -4,6 +4,65 @@ import type { PluginConfig, MemoryRelayClient, ConversationMessage, SessionResol
4
4
  import { buildRequestContext } from "../context/request-context.js";
5
5
  import { runPipeline } from "../pipelines/runner.js";
6
6
  import { capturePipeline } from "../pipelines/capture/index.js";
7
+ import { autoSessionMap, DECISION_KEYWORDS } from "./auto-session-store.js";
8
+
9
+ /**
10
+ * Extract potential decisions from conversation messages using keyword heuristics.
11
+ * Returns an array of { title, rationale } for each detected decision.
12
+ */
13
+ export function extractDecisions(
14
+ messages: ConversationMessage[],
15
+ ): Array<{ title: string; rationale: string }> {
16
+ const decisions: Array<{ title: string; rationale: string }> = [];
17
+ const seen = new Set<string>();
18
+
19
+ for (const msg of messages) {
20
+ if (msg.role !== "assistant") continue;
21
+ const content = msg.content;
22
+ const lower = content.toLowerCase();
23
+
24
+ for (const keyword of DECISION_KEYWORDS) {
25
+ if (!lower.includes(keyword)) continue;
26
+
27
+ // Find the sentence containing the keyword
28
+ const sentences = content.split(/[.!?\n]+/).filter((s) => s.trim().length > 10);
29
+ for (const sentence of sentences) {
30
+ if (!sentence.toLowerCase().includes(keyword)) continue;
31
+ const trimmed = sentence.trim();
32
+ // Avoid duplicates and very long passages
33
+ if (trimmed.length > 500) continue;
34
+ const key = trimmed.slice(0, 80).toLowerCase();
35
+ if (seen.has(key)) continue;
36
+ seen.add(key);
37
+
38
+ decisions.push({
39
+ title: trimmed.slice(0, 200),
40
+ rationale: `Auto-detected from conversation (keyword: "${keyword}"): ${trimmed}`,
41
+ });
42
+ break; // One decision per keyword per message
43
+ }
44
+ if (decisions.length >= 5) break; // Cap at 5 decisions per session
45
+ }
46
+ if (decisions.length >= 5) break;
47
+ }
48
+ return decisions;
49
+ }
50
+
51
+ /**
52
+ * Generate a summary from the last few significant assistant messages.
53
+ */
54
+ export function generateSessionSummary(messages: ConversationMessage[]): string {
55
+ const assistantMessages = messages
56
+ .filter((m) => m.role === "assistant" && m.content.length > 30)
57
+ .slice(-3);
58
+
59
+ if (assistantMessages.length === 0) return "Session completed.";
60
+
61
+ return assistantMessages
62
+ .map((m) => m.content.slice(0, 300))
63
+ .join(" | ")
64
+ .slice(0, 800);
65
+ }
7
66
 
8
67
  export function registerAgentEnd(
9
68
  api: OpenClawPluginApi,
@@ -18,28 +77,67 @@ export function registerAgentEnd(
18
77
  api.on("agent_end", async (event) => {
19
78
  if (!event.success || !event.messages || event.messages.length === 0) return;
20
79
 
21
- try {
22
- const messages: ConversationMessage[] = [];
23
- for (const msg of event.messages) {
24
- if (!msg || typeof msg !== "object") continue;
25
- const msgObj = msg as Record<string, unknown>;
26
- const role = msgObj.role as string;
27
- if (role !== "user" && role !== "assistant") continue;
28
-
29
- const content = msgObj.content;
30
- if (typeof content === "string") {
31
- messages.push({ role: role as "user" | "assistant", content });
32
- } else if (Array.isArray(content)) {
33
- for (const block of content) {
34
- if (block && typeof block === "object" && (block as any).type === "text" && (block as any).text) {
35
- messages.push({ role: role as "user" | "assistant", content: (block as any).text });
36
- }
80
+ // Parse messages first (shared by session lifecycle and capture pipeline)
81
+ const messages: ConversationMessage[] = [];
82
+ for (const msg of event.messages) {
83
+ if (!msg || typeof msg !== "object") continue;
84
+ const msgObj = msg as Record<string, unknown>;
85
+ const role = msgObj.role as string;
86
+ if (role !== "user" && role !== "assistant") continue;
87
+
88
+ const content = msgObj.content;
89
+ if (typeof content === "string") {
90
+ messages.push({ role: role as "user" | "assistant", content });
91
+ } else if (Array.isArray(content)) {
92
+ for (const block of content) {
93
+ if (block && typeof block === "object" && (block as any).type === "text" && (block as any).text) {
94
+ messages.push({ role: role as "user" | "assistant", content: (block as any).text });
37
95
  }
38
96
  }
39
97
  }
98
+ }
99
+
100
+ if (messages.length === 0) return;
101
+
102
+ // --- Auto session lifecycle: decisions + session_end ---
103
+ const sessionKey = event.ctx?.sessionKey || event.sessionId || "";
104
+ const sessionId = autoSessionMap.get(sessionKey);
105
+
106
+ if (sessionId) {
107
+ try {
108
+ // Extract and record decisions
109
+ const decisions = extractDecisions(messages);
110
+ const projectSlug = config.defaultProject || process.env.MEMORYRELAY_DEFAULT_PROJECT;
40
111
 
41
- if (messages.length === 0) return;
112
+ for (const decision of decisions) {
113
+ try {
114
+ await client.recordDecision(
115
+ decision.title,
116
+ decision.rationale,
117
+ undefined,
118
+ projectSlug,
119
+ ["auto-detected"],
120
+ undefined,
121
+ { source: "auto-session-lifecycle", session_id: sessionId },
122
+ );
123
+ } catch (err) {
124
+ api.logger.warn?.(`memory-memoryrelay: auto decision_record failed: ${String(err)}`);
125
+ }
126
+ }
127
+
128
+ // End session with summary
129
+ const summary = generateSessionSummary(messages);
130
+ await client.endSession(sessionId, summary);
131
+ api.logger.debug?.(`memory-memoryrelay: auto-session ended ${sessionId}`);
132
+ } catch (err) {
133
+ api.logger.warn?.(`memory-memoryrelay: auto session_end failed (non-blocking): ${String(err)}`);
134
+ } finally {
135
+ autoSessionMap.delete(sessionKey);
136
+ }
137
+ }
42
138
 
139
+ // --- Existing capture pipeline ---
140
+ try {
43
141
  const requestCtx = buildRequestContext(event, config);
44
142
  const pipelineCtx = { requestCtx, config, client, sessionResolver, localCache, syncDaemon };
45
143
  await runPipeline(capturePipeline, { messages }, pipelineCtx);
@@ -0,0 +1,16 @@
1
+ // src/hooks/auto-session-store.ts
2
+ // Shared state between before-agent-start and agent-end hooks for auto session lifecycle.
3
+
4
+ /** Maps agent session key → MemoryRelay session ID */
5
+ export const autoSessionMap = new Map<string, string>();
6
+
7
+ /** Decision detection keywords used in agent-end heuristics */
8
+ export const DECISION_KEYWORDS = [
9
+ "decided",
10
+ "going with",
11
+ "architecture",
12
+ "we will",
13
+ "won't",
14
+ "instead of",
15
+ "chosen",
16
+ ];
@@ -1,10 +1,28 @@
1
1
  // src/hooks/before-agent-start.ts
2
+ import { basename } from "node:path";
2
3
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
- import type { PluginConfig } from "../pipelines/types.js";
4
+ import type { PluginConfig, MemoryRelayClient } from "../pipelines/types.js";
5
+ import { autoSessionMap } from "./auto-session-store.js";
6
+
7
+ /**
8
+ * Resolve project slug from config, env, or working directory name.
9
+ */
10
+ function resolveProjectSlug(config: PluginConfig, defaultProject: string | undefined): string | undefined {
11
+ if (defaultProject) return defaultProject;
12
+ if (config.defaultProject) return config.defaultProject;
13
+ const envProject = process.env.MEMORYRELAY_DEFAULT_PROJECT;
14
+ if (envProject) return envProject;
15
+ try {
16
+ return basename(process.cwd());
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
4
21
 
5
22
  export function registerBeforeAgentStart(
6
23
  api: OpenClawPluginApi,
7
24
  config: PluginConfig,
25
+ client: MemoryRelayClient,
8
26
  isToolEnabled: (name: string) => boolean,
9
27
  defaultProject: string | undefined,
10
28
  ): void {
@@ -24,6 +42,62 @@ export function registerBeforeAgentStart(
24
42
  }
25
43
  }
26
44
 
45
+ // --- Auto session lifecycle: session_start + project_context ---
46
+ const projectSlug = resolveProjectSlug(config, defaultProject);
47
+ let projectContextBlock = "";
48
+
49
+ try {
50
+ const sessionKey = event.ctx?.sessionKey || event.sessionId || "";
51
+
52
+ // Start a tracked session (non-blocking — we await but don't let failure block the turn)
53
+ const today = new Date().toISOString().slice(0, 10);
54
+ const sessionResult = await client.startSession(
55
+ `Auto session ${today}`,
56
+ projectSlug,
57
+ { source: "openclaw-plugin", trigger: "before_agent_start" },
58
+ );
59
+
60
+ if (sessionResult?.id && sessionKey) {
61
+ autoSessionMap.set(sessionKey, sessionResult.id);
62
+ api.logger.debug?.(`memory-memoryrelay: auto-session started ${sessionResult.id}`);
63
+ }
64
+ } catch (err) {
65
+ api.logger.warn?.(`memory-memoryrelay: auto session_start failed (non-blocking): ${String(err)}`);
66
+ }
67
+
68
+ // Load project context (hot memories, decisions, patterns)
69
+ if (projectSlug) {
70
+ try {
71
+ const ctx = await client.getProjectContext(projectSlug);
72
+ if (ctx) {
73
+ const parts: string[] = [];
74
+ if (ctx.hot_memories?.length) {
75
+ parts.push("### Hot Memories");
76
+ for (const m of ctx.hot_memories.slice(0, 10)) {
77
+ parts.push(`- ${m.content ?? m}`);
78
+ }
79
+ }
80
+ if (ctx.recent_decisions?.length) {
81
+ parts.push("### Active Decisions");
82
+ for (const d of ctx.recent_decisions.slice(0, 5)) {
83
+ parts.push(`- **${d.title}**: ${(d.rationale ?? "").slice(0, 200)}`);
84
+ }
85
+ }
86
+ if (ctx.active_patterns?.length) {
87
+ parts.push("### Adopted Patterns");
88
+ for (const p of ctx.active_patterns.slice(0, 5)) {
89
+ parts.push(`- **${p.title}**: ${(p.description ?? "").slice(0, 150)}`);
90
+ }
91
+ }
92
+ if (parts.length > 0) {
93
+ projectContextBlock = `\n\n## Project Context (${projectSlug})\n\n${parts.join("\n")}`;
94
+ }
95
+ }
96
+ } catch (err) {
97
+ api.logger.warn?.(`memory-memoryrelay: project_context failed (non-blocking): ${String(err)}`);
98
+ }
99
+ }
100
+
27
101
  // Build workflow instructions dynamically based on enabled tools
28
102
  const lines: string[] = [
29
103
  "You have MemoryRelay tools available for persistent memory across sessions.",
@@ -102,7 +176,7 @@ export function registerBeforeAgentStart(
102
176
 
103
177
  const workflowInstructions = lines.join("\n");
104
178
 
105
- const prependContext = `<memoryrelay-workflow>\n${workflowInstructions}\n</memoryrelay-workflow>`;
179
+ const prependContext = `<memoryrelay-workflow>\n${workflowInstructions}${projectContextBlock}\n</memoryrelay-workflow>`;
106
180
 
107
181
  return { prependContext };
108
182
  });
@@ -109,7 +109,18 @@ export interface MemoryRelayClient {
109
109
  project?: string,
110
110
  metadata?: Record<string, string>,
111
111
  ): Promise<{ id: string }>;
112
+ startSession(title?: string, project?: string, metadata?: Record<string, string>): Promise<{ id: string }>;
112
113
  endSession(sessionId: string, summary?: string): Promise<void>;
114
+ getProjectContext(project: string): Promise<any>;
115
+ recordDecision(
116
+ title: string,
117
+ rationale: string,
118
+ alternatives?: string,
119
+ project?: string,
120
+ tags?: string[],
121
+ status?: string,
122
+ metadata?: Record<string, string>,
123
+ ): Promise<any>;
113
124
  }
114
125
 
115
126
  export interface SessionResolverLike {