@chrysb/alphaclaw 0.9.0-beta.0 → 0.9.0-beta.2

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,17 +26,34 @@ const resolveGatewayWsUrl = ({ openclawDir, gatewayPort }) => {
26
26
  return `${scheme}://127.0.0.1:${gatewayPort}`;
27
27
  };
28
28
 
29
+ const sessions = new Map();
29
30
  let activeTransport = null;
31
+ const kSessionGraceMs = 15_000;
30
32
 
31
- const closeActiveTransport = async () => {
32
- if (!activeTransport) return;
33
- const t = activeTransport;
33
+ const closeSession = (sessionId) => {
34
+ const t = sessions.get(sessionId);
35
+ if (!t) return;
36
+ sessions.delete(sessionId);
37
+ if (activeTransport === t) activeTransport = null;
38
+ t.close().catch(() => {});
39
+ };
40
+
41
+ const closeAllSessions = () => {
42
+ for (const [id] of sessions) closeSession(id);
34
43
  activeTransport = null;
35
- try {
36
- await t.close();
37
- } catch {
38
- /* already closed */
39
- }
44
+ };
45
+
46
+ const retireStaleSessions = (keepId) => {
47
+ const staleIds = [...sessions.keys()].filter((id) => id !== keepId);
48
+ if (staleIds.length === 0) return;
49
+ setTimeout(() => {
50
+ for (const id of staleIds) {
51
+ if (sessions.has(id) && sessions.get(id) !== activeTransport) {
52
+ console.log(`[mcp] Cleaning up stale session: ${id}`);
53
+ closeSession(id);
54
+ }
55
+ }
56
+ }, kSessionGraceMs);
40
57
  };
41
58
 
42
59
  const registerMcpRoutes = ({
@@ -46,7 +63,6 @@ const registerMcpRoutes = ({
46
63
  gatewayEnv,
47
64
  openclawDir,
48
65
  }) => {
49
- // Wire bridge stdout messages → active transport
50
66
  setOnMcpMessage((message) => {
51
67
  if (!activeTransport) return;
52
68
  activeTransport.send(message).catch((err) => {
@@ -86,7 +102,7 @@ const registerMcpRoutes = ({
86
102
  });
87
103
 
88
104
  app.post("/api/mcp/stop", requireAuth, async (_req, res) => {
89
- await closeActiveTransport();
105
+ closeAllSessions();
90
106
  const result = stopMcpBridge();
91
107
  res.json(result);
92
108
  });
@@ -125,38 +141,52 @@ const registerMcpRoutes = ({
125
141
  const sessionId = req.headers["mcp-session-id"];
126
142
 
127
143
  // ── Existing session ───────────────────────────────────────
128
- if (sessionId && activeTransport) {
129
- try {
130
- await activeTransport.handleRequest(req, res, req.body);
131
- } catch (err) {
132
- console.error("[mcp] handleRequest error (existing session):", err?.message);
133
- if (!res.headersSent) {
134
- res.status(500).json({ error: "Internal transport error" });
144
+ if (sessionId) {
145
+ const transport = sessions.get(sessionId);
146
+ if (transport) {
147
+ console.log(
148
+ `[mcp] ${req.method} sessionId=${sessionId} routed to transport (sessions=${sessions.size})`,
149
+ );
150
+ try {
151
+ await transport.handleRequest(req, res, req.body);
152
+ } catch (err) {
153
+ console.error(
154
+ "[mcp] handleRequest error (existing session):",
155
+ err?.message,
156
+ );
157
+ if (!res.headersSent) {
158
+ res.status(500).json({ error: "Internal transport error" });
159
+ }
135
160
  }
161
+ } else {
162
+ console.log(
163
+ `[mcp] ${req.method} sessionId=${sessionId} → NOT FOUND (known=[${[...sessions.keys()].join(", ")}])`,
164
+ );
165
+ res.status(404).json({
166
+ jsonrpc: "2.0",
167
+ error: {
168
+ code: -32001,
169
+ message: "Session not found. The server may have been restarted.",
170
+ },
171
+ id: null,
172
+ });
136
173
  }
137
174
  return;
138
175
  }
139
176
 
140
- // ── Stale / unknown session ────────────────────────────────
141
- if (sessionId && !activeTransport) {
142
- res.status(404).json({
143
- jsonrpc: "2.0",
144
- error: {
145
- code: -32000,
146
- message: "Session not found. The server may have been restarted.",
147
- },
148
- id: null,
149
- });
150
- return;
151
- }
152
-
153
177
  // ── New session (POST without session ID) ────────────────
154
- // Let the transport validate the body internally — it will reject
155
- // non-initialize requests and return the proper JSON-RPC error.
156
- if (!sessionId && req.method === "POST") {
178
+ if (req.method === "POST") {
157
179
  const transport = new StreamableHTTPServerTransport({
158
180
  sessionIdGenerator: () => randomUUID(),
159
181
  enableJsonResponse: true,
182
+ onsessioninitialized: (newSessionId) => {
183
+ sessions.set(newSessionId, transport);
184
+ activeTransport = transport;
185
+ retireStaleSessions(newSessionId);
186
+ console.log(
187
+ `[mcp] Session registered: ${newSessionId} (sessions=${sessions.size})`,
188
+ );
189
+ },
160
190
  });
161
191
 
162
192
  transport.onmessage = (message) => {
@@ -164,10 +194,14 @@ const registerMcpRoutes = ({
164
194
  };
165
195
 
166
196
  transport.onclose = () => {
167
- if (activeTransport === transport) {
168
- activeTransport = null;
169
- console.log("[mcp] Transport closed");
197
+ for (const [id, t] of sessions) {
198
+ if (t === transport) {
199
+ sessions.delete(id);
200
+ break;
201
+ }
170
202
  }
203
+ if (activeTransport === transport) activeTransport = null;
204
+ console.log(`[mcp] Transport closed (sessions=${sessions.size})`);
171
205
  };
172
206
 
173
207
  transport.onerror = (err) => {
@@ -176,27 +210,17 @@ const registerMcpRoutes = ({
176
210
 
177
211
  await transport.start();
178
212
 
179
- // Set activeTransport BEFORE handleRequest so the onMcpMessage
180
- // callback can forward the child's response to this transport.
181
- const prev = activeTransport;
182
- activeTransport = transport;
183
-
184
213
  try {
185
214
  await transport.handleRequest(req, res, req.body);
186
215
  } catch (err) {
187
- console.error("[mcp] handleRequest error (new session):", err?.message);
216
+ console.error(
217
+ "[mcp] handleRequest error (new session):",
218
+ err?.message,
219
+ );
188
220
  if (!res.headersSent) {
189
221
  res.status(500).json({ error: "Failed to initialize MCP session" });
190
222
  }
191
223
  }
192
-
193
- if (transport.sessionId) {
194
- if (prev) prev.close().catch(() => {});
195
- console.log(`[mcp] Session established: ${transport.sessionId}`);
196
- } else {
197
- // Session was not established — roll back
198
- if (activeTransport === transport) activeTransport = prev;
199
- }
200
224
  return;
201
225
  }
202
226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.0-beta.0",
3
+ "version": "0.9.0-beta.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },