@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 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: 'openclaw-main',
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
- handleEvent({ type, payload: e, context: c }, config, reliability, logger);
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
- handleEvent({ type: 'message_sent', payload: { content }, context: ctx }, config, reliability, logger);
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
- return handleContextInjection(client, config, logger, e.prompt || e.message || '');
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
- return mapMessageReceived(event.payload, timestamp, sensitivity);
115
+ mapped = mapMessageReceived(event.payload, timestamp, sensitivity);
116
+ break;
114
117
  case 'message_sent':
115
118
  case 'message_sending':
116
- return mapMessageSent(event.payload, timestamp, sensitivity);
119
+ mapped = mapMessageSent(event.payload, timestamp, sensitivity);
120
+ break;
117
121
  case 'session_start':
118
- return mapSessionStart(timestamp, sensitivity);
122
+ mapped = mapSessionStart(timestamp, sensitivity);
123
+ break;
119
124
  case 'after_tool_call':
120
- return mapToolCall(event.payload, timestamp, sensitivity);
125
+ mapped = mapToolCall(event.payload, timestamp, sensitivity);
126
+ break;
121
127
  case 'fact_extracted':
122
- return mapFactExtracted(event.payload, timestamp, sensitivity);
128
+ mapped = mapFactExtracted(event.payload, timestamp, sensitivity);
129
+ break;
123
130
  case 'task_completed':
124
- return mapTaskCompleted(event.payload, timestamp);
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.5",
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",