@vainplex/openclaw-membrane 0.3.5 → 0.3.7
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/CHANGELOG.md +13 -0
- package/dist/index.js +19 -13
- package/dist/mapping.js +19 -7
- package/dist/parser.js +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.7] — 2026-03-04
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Full agent memory isolation (client-side filtering).** Server-side scope filters in `membraned` are not enforced, so v0.3.6's retrieval scoping alone was insufficient. Now:
|
|
7
|
+
- **Ingest:** Events tagged with `source: openclaw-{agentId}` (was always `openclaw`).
|
|
8
|
+
- **Retrieve:** Client-side filter in `parseMembraneRecords()` drops records from other agents. Legacy records (source=`openclaw`) pass through for backwards compatibility.
|
|
9
|
+
- Agents can no longer see each other's conversation history in Membrane context injection.
|
|
10
|
+
|
|
11
|
+
## [0.3.6] — 2026-03-04
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Agent-scoped memory retrieval.** Membrane now passes `agentId` from hook context to retrieval queries. Each agent only retrieves its own memories instead of seeing all agents' conversations. Prevents cross-agent memory injection in multi-agent setups. `actor_id` is set to `openclaw-{agentId}` and `scopes` filters by agent.
|
|
15
|
+
|
|
3
16
|
## [0.3.5] — 2026-03-04
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -41,14 +41,14 @@ export function validateConfig(raw) {
|
|
|
41
41
|
return result;
|
|
42
42
|
}
|
|
43
43
|
// --- Retrieve helper ---
|
|
44
|
-
async function retrieveMemories(client, query, config, fetchLimit) {
|
|
44
|
+
async function retrieveMemories(client, query, config, fetchLimit, agentId = 'main') {
|
|
45
45
|
const request = {
|
|
46
46
|
task_descriptor: query.substring(0, 500),
|
|
47
47
|
trust: {
|
|
48
48
|
max_sensitivity: config.retrieve_max_sensitivity,
|
|
49
49
|
authenticated: true,
|
|
50
|
-
actor_id:
|
|
51
|
-
scopes: [],
|
|
50
|
+
actor_id: `openclaw-${agentId}`,
|
|
51
|
+
scopes: [agentId],
|
|
52
52
|
},
|
|
53
53
|
memory_types: [],
|
|
54
54
|
min_salience: config.retrieve_min_salience,
|
|
@@ -90,15 +90,15 @@ async function handleSearch(client, config, logger, params) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
// --- Context hook handler ---
|
|
93
|
-
async function handleContextInjection(client, config, logger, prompt) {
|
|
93
|
+
async function handleContextInjection(client, config, logger, prompt, agentId = 'main') {
|
|
94
94
|
if (!prompt || prompt.length < 5)
|
|
95
95
|
return undefined;
|
|
96
96
|
try {
|
|
97
97
|
const fetchLimit = Math.max(config.retrieve_limit * 10, 50);
|
|
98
|
-
const result = await retrieveMemories(client, prompt, config, fetchLimit);
|
|
98
|
+
const result = await retrieveMemories(client, prompt, config, fetchLimit, agentId);
|
|
99
99
|
if (!result?.records?.length)
|
|
100
100
|
return undefined;
|
|
101
|
-
const parsed = parseMembraneRecords(result.records, logger);
|
|
101
|
+
const parsed = parseMembraneRecords(result.records, logger, agentId);
|
|
102
102
|
const memories = selectMemories(parsed, config.retrieve_limit);
|
|
103
103
|
if (memories.length === 0)
|
|
104
104
|
return undefined;
|
|
@@ -119,16 +119,16 @@ async function handleContextInjection(client, config, logger, prompt) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
// --- Event handler ---
|
|
122
|
-
function handleEvent(event, config, reliability, logger) {
|
|
122
|
+
function handleEvent(event, config, reliability, logger, agentId = 'main') {
|
|
123
123
|
const normalizedEvent = {
|
|
124
124
|
type: event.type,
|
|
125
125
|
payload: (event.payload || event.data || {}),
|
|
126
126
|
context: event.context,
|
|
127
127
|
};
|
|
128
128
|
const sensitivity = mapSensitivity(normalizedEvent, config.default_sensitivity);
|
|
129
|
-
const mapped = mapEvent(normalizedEvent, sensitivity);
|
|
129
|
+
const mapped = mapEvent(normalizedEvent, sensitivity, agentId);
|
|
130
130
|
if (mapped) {
|
|
131
|
-
logger.info(`[membrane] Received event: ${event.type}`);
|
|
131
|
+
logger.info(`[membrane] Received event: ${event.type} (agent=${agentId})`);
|
|
132
132
|
reliability.enqueue(mapped.method, mapped.payload);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -163,7 +163,9 @@ const plugin = {
|
|
|
163
163
|
const hookHandler = (type) => (event, ctx) => {
|
|
164
164
|
const e = event;
|
|
165
165
|
const c = ctx;
|
|
166
|
-
|
|
166
|
+
const hookCtx = ctx;
|
|
167
|
+
const agentId = hookCtx?.agentId || 'main';
|
|
168
|
+
handleEvent({ type, payload: e, context: c }, config, reliability, logger, agentId);
|
|
167
169
|
};
|
|
168
170
|
api.on('message_received', hookHandler('message_received'));
|
|
169
171
|
api.on('message_sent', hookHandler('message_sent'));
|
|
@@ -192,7 +194,9 @@ const plugin = {
|
|
|
192
194
|
: '';
|
|
193
195
|
if (!content || content.length < 10)
|
|
194
196
|
break;
|
|
195
|
-
|
|
197
|
+
const endCtx = ctx;
|
|
198
|
+
const endAgentId = endCtx?.agentId || 'main';
|
|
199
|
+
handleEvent({ type: 'message_sent', payload: { content }, context: ctx }, config, reliability, logger, endAgentId);
|
|
196
200
|
break; // Only the last one
|
|
197
201
|
}
|
|
198
202
|
});
|
|
@@ -209,9 +213,11 @@ const plugin = {
|
|
|
209
213
|
});
|
|
210
214
|
// Context hook: auto-inject Membrane memories before agent starts
|
|
211
215
|
if (config.retrieve_enabled) {
|
|
212
|
-
api.on('before_agent_start', (event) => {
|
|
216
|
+
api.on('before_agent_start', (event, ctx) => {
|
|
213
217
|
const e = event;
|
|
214
|
-
|
|
218
|
+
const hookCtx = ctx;
|
|
219
|
+
const agentId = hookCtx?.agentId || 'main';
|
|
220
|
+
return handleContextInjection(client, config, logger, e.prompt || e.message || '', agentId);
|
|
215
221
|
});
|
|
216
222
|
}
|
|
217
223
|
// Shutdown: flush buffer, close client
|
package/dist/mapping.js
CHANGED
|
@@ -106,23 +106,35 @@ function mapTaskCompleted(payload, timestamp) {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
// --- Main dispatcher ---
|
|
109
|
-
export function mapEvent(event, sensitivity) {
|
|
109
|
+
export function mapEvent(event, sensitivity, agentId = 'main') {
|
|
110
110
|
const timestamp = new Date().toISOString();
|
|
111
|
+
const source = `openclaw-${agentId}`;
|
|
112
|
+
let mapped;
|
|
111
113
|
switch (event.type) {
|
|
112
114
|
case 'message_received':
|
|
113
|
-
|
|
115
|
+
mapped = mapMessageReceived(event.payload, timestamp, sensitivity);
|
|
116
|
+
break;
|
|
114
117
|
case 'message_sent':
|
|
115
118
|
case 'message_sending':
|
|
116
|
-
|
|
119
|
+
mapped = mapMessageSent(event.payload, timestamp, sensitivity);
|
|
120
|
+
break;
|
|
117
121
|
case 'session_start':
|
|
118
|
-
|
|
122
|
+
mapped = mapSessionStart(timestamp, sensitivity);
|
|
123
|
+
break;
|
|
119
124
|
case 'after_tool_call':
|
|
120
|
-
|
|
125
|
+
mapped = mapToolCall(event.payload, timestamp, sensitivity);
|
|
126
|
+
break;
|
|
121
127
|
case 'fact_extracted':
|
|
122
|
-
|
|
128
|
+
mapped = mapFactExtracted(event.payload, timestamp, sensitivity);
|
|
129
|
+
break;
|
|
123
130
|
case 'task_completed':
|
|
124
|
-
|
|
131
|
+
mapped = mapTaskCompleted(event.payload, timestamp);
|
|
132
|
+
break;
|
|
125
133
|
default:
|
|
126
134
|
return null;
|
|
127
135
|
}
|
|
136
|
+
if (mapped) {
|
|
137
|
+
mapped.payload.source = source;
|
|
138
|
+
}
|
|
139
|
+
return mapped;
|
|
128
140
|
}
|
package/dist/parser.js
CHANGED
|
@@ -10,13 +10,22 @@ const MIN_TOOL_SUMMARY_LENGTH = 30;
|
|
|
10
10
|
const MAX_SUMMARY_LENGTH = 500;
|
|
11
11
|
/**
|
|
12
12
|
* Parse raw Membrane Retrieve records into categorized, formatted memory strings.
|
|
13
|
+
* When agentId is provided, filters records to only include those from the same agent.
|
|
14
|
+
* Records without a source tag (legacy) are included for backwards compatibility.
|
|
13
15
|
*/
|
|
14
|
-
export function parseMembraneRecords(rawRecords, logger) {
|
|
16
|
+
export function parseMembraneRecords(rawRecords, logger, agentId) {
|
|
15
17
|
const conversational = [];
|
|
16
18
|
const tool = [];
|
|
19
|
+
const expectedSource = agentId ? `openclaw-${agentId}` : undefined;
|
|
20
|
+
let filtered = 0;
|
|
17
21
|
for (const raw of rawRecords) {
|
|
18
22
|
try {
|
|
19
23
|
const record = JSON.parse(Buffer.from(raw).toString());
|
|
24
|
+
// Client-side agent isolation: skip records from other agents
|
|
25
|
+
if (expectedSource && record.source && record.source !== expectedSource && record.source !== 'openclaw') {
|
|
26
|
+
filtered++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
20
29
|
const tsShort = formatTimestamp(record.created_at);
|
|
21
30
|
parsePayload(record, tsShort, conversational, tool);
|
|
22
31
|
}
|
|
@@ -24,6 +33,9 @@ export function parseMembraneRecords(rawRecords, logger) {
|
|
|
24
33
|
logger?.debug(`[membrane] Failed to parse record: ${err instanceof Error ? err.message : String(err)}`);
|
|
25
34
|
}
|
|
26
35
|
}
|
|
36
|
+
if (filtered > 0) {
|
|
37
|
+
logger?.debug(`[membrane] Filtered ${filtered} records from other agents (keeping source=${expectedSource})`);
|
|
38
|
+
}
|
|
27
39
|
return { conversational, tool };
|
|
28
40
|
}
|
|
29
41
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vainplex/openclaw-membrane",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Membrane gRPC bridge for OpenClaw — episodic memory ingestion, search tool, and auto-context injection via Membrane sidecar",
|
|
6
6
|
"main": "dist/index.js",
|