@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 +3 -3
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -2
- package/src/client/memoryrelay-client.ts +1 -1
- package/src/hooks/agent-end.ts +115 -17
- package/src/hooks/auto-session-store.ts +16 -0
- package/src/hooks/before-agent-start.ts +76 -2
- package/src/pipelines/types.ts +11 -0
package/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenClaw Memory Plugin - MemoryRelay
|
|
3
|
-
* Version: 0.
|
|
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.
|
|
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
|
// ========================================================================
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"id": "plugin-memoryrelay-ai",
|
|
3
3
|
"kind": "memory",
|
|
4
4
|
"name": "MemoryRelay AI",
|
|
5
|
-
"description": "MemoryRelay v0.
|
|
6
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
156
|
+
"User-Agent": "openclaw-memory-memoryrelay/0.18.0",
|
|
157
157
|
},
|
|
158
158
|
body: body ? JSON.stringify(body) : undefined,
|
|
159
159
|
},
|
package/src/hooks/agent-end.ts
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/pipelines/types.ts
CHANGED
|
@@ -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 {
|