@chrysb/alphaclaw 0.9.0-beta.6 → 0.9.0-beta.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.
@@ -26,28 +26,57 @@ const resolveGatewayWsUrl = ({ openclawDir, gatewayPort }) => {
26
26
  return `${scheme}://127.0.0.1:${gatewayPort}`;
27
27
  };
28
28
 
29
- let activeSessionId = null;
29
+ const sessions = new Map();
30
30
  let activeTransport = null;
31
31
  const kSseKeepAliveMs = 15_000;
32
+ const kMaxSessions = 8;
32
33
 
33
- const closeActiveSession = () => {
34
- if (!activeTransport) return;
35
- const id = activeSessionId;
36
- const t = activeTransport;
37
- activeSessionId = null;
38
- activeTransport = null;
39
- t.close().catch(() => {});
40
- if (id) console.log(`[mcp] Closed session: ${id}`);
41
- };
34
+ let nextBridgeId = 1;
35
+ const pendingRequests = new Map();
42
36
 
43
37
  const adoptSession = (sessionId, transport) => {
44
- if (activeTransport && activeTransport !== transport) {
45
- const prevId = activeSessionId;
46
- activeTransport.close().catch(() => {});
47
- console.log(`[mcp] Replaced session ${prevId} → ${sessionId}`);
48
- }
49
- activeSessionId = sessionId;
38
+ sessions.set(sessionId, transport);
50
39
  activeTransport = transport;
40
+
41
+ if (sessions.size > kMaxSessions) {
42
+ const oldestId = sessions.keys().next().value;
43
+ if (oldestId !== sessionId) {
44
+ const old = sessions.get(oldestId);
45
+ sessions.delete(oldestId);
46
+ old.close().catch(() => {});
47
+ console.log(`[mcp] Evicted oldest session: ${oldestId}`);
48
+ }
49
+ }
50
+ };
51
+
52
+ const forwardToBridge = (message, transport) => {
53
+ if (message.id != null) {
54
+ const bridgeId = nextBridgeId++;
55
+ pendingRequests.set(bridgeId, { originalId: message.id, transport });
56
+ writeToMcpBridge({ ...message, id: bridgeId });
57
+ } else {
58
+ writeToMcpBridge(message);
59
+ }
60
+ };
61
+
62
+ const cleanupTransport = (transport) => {
63
+ for (const [id, t] of sessions) {
64
+ if (t === transport) {
65
+ sessions.delete(id);
66
+ break;
67
+ }
68
+ }
69
+ for (const [bridgeId, pending] of pendingRequests) {
70
+ if (pending.transport === transport) pendingRequests.delete(bridgeId);
71
+ }
72
+ if (activeTransport === transport) activeTransport = null;
73
+ };
74
+
75
+ const closeAllSessions = () => {
76
+ for (const [, t] of sessions) t.close().catch(() => {});
77
+ sessions.clear();
78
+ pendingRequests.clear();
79
+ activeTransport = null;
51
80
  };
52
81
 
53
82
  const registerMcpRoutes = ({
@@ -58,10 +87,21 @@ const registerMcpRoutes = ({
58
87
  openclawDir,
59
88
  }) => {
60
89
  setOnMcpMessage((message) => {
61
- if (!activeTransport) return;
62
- activeTransport.send(message).catch((err) => {
63
- console.error("[mcp] Failed to forward to transport:", err?.message);
64
- });
90
+ if (message.id != null) {
91
+ const pending = pendingRequests.get(message.id);
92
+ if (pending) {
93
+ pendingRequests.delete(message.id);
94
+ pending.transport
95
+ .send({ ...message, id: pending.originalId })
96
+ .catch((err) => {
97
+ console.error("[mcp] Failed to forward response:", err?.message);
98
+ });
99
+ }
100
+ return;
101
+ }
102
+ if (activeTransport) {
103
+ activeTransport.send(message).catch(() => {});
104
+ }
65
105
  });
66
106
 
67
107
  // ── Internal API (session auth) ────────────────────────────────
@@ -96,7 +136,7 @@ const registerMcpRoutes = ({
96
136
  });
97
137
 
98
138
  app.post("/api/mcp/stop", requireAuth, async (_req, res) => {
99
- closeActiveSession();
139
+ closeAllSessions();
100
140
  const result = stopMcpBridge();
101
141
  res.json(result);
102
142
  });
@@ -146,12 +186,13 @@ const registerMcpRoutes = ({
146
186
 
147
187
  // ── Existing session ───────────────────────────────────────
148
188
  if (sessionId) {
149
- if (sessionId === activeSessionId && activeTransport) {
189
+ const transport = sessions.get(sessionId);
190
+ if (transport) {
150
191
  console.log(
151
- `[mcp] ${req.method} sessionId=${sessionId} → active session`,
192
+ `[mcp] ${req.method} sessionId=${sessionId} → routed (sessions=${sessions.size})`,
152
193
  );
153
194
  try {
154
- await activeTransport.handleRequest(req, res, req.body);
195
+ await transport.handleRequest(req, res, req.body);
155
196
  } catch (err) {
156
197
  console.error(
157
198
  "[mcp] handleRequest error (existing session):",
@@ -163,7 +204,7 @@ const registerMcpRoutes = ({
163
204
  }
164
205
  } else {
165
206
  console.log(
166
- `[mcp] ${req.method} sessionId=${sessionId} → NOT FOUND (active=${activeSessionId || "none"})`,
207
+ `[mcp] ${req.method} sessionId=${sessionId} → NOT FOUND (sessions=${sessions.size})`,
167
208
  );
168
209
  res.status(404).json({
169
210
  jsonrpc: "2.0",
@@ -184,20 +225,19 @@ const registerMcpRoutes = ({
184
225
  enableJsonResponse: true,
185
226
  onsessioninitialized: (newSessionId) => {
186
227
  adoptSession(newSessionId, transport);
187
- console.log(`[mcp] Session adopted: ${newSessionId}`);
228
+ console.log(
229
+ `[mcp] Session adopted: ${newSessionId} (sessions=${sessions.size})`,
230
+ );
188
231
  },
189
232
  });
190
233
 
191
234
  transport.onmessage = (message) => {
192
- writeToMcpBridge(message);
235
+ forwardToBridge(message, transport);
193
236
  };
194
237
 
195
238
  transport.onclose = () => {
196
- if (activeTransport === transport) {
197
- activeSessionId = null;
198
- activeTransport = null;
199
- }
200
- console.log(`[mcp] Transport closed`);
239
+ cleanupTransport(transport);
240
+ console.log(`[mcp] Transport closed (sessions=${sessions.size})`);
201
241
  };
202
242
 
203
243
  transport.onerror = (err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.0-beta.6",
3
+ "version": "0.9.0-beta.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },