Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit 1c96234

Browse files
authored
Merge pull request #30 from sotopia-lab/unique_sessions
Multi session management update
2 parents 43f4f11 + 5a537f4 commit 1c96234

File tree

17 files changed

+787
-182
lines changed

17 files changed

+787
-182
lines changed

backend/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"cors": "^2.8.5",
1313
"express": "^4.18.2",
1414
"redis": "^4.6.12",
15-
"socket.io": "^4.8.1"
15+
"socket.io": "^4.8.1",
16+
"uuid": "^11.0.5"
1617
}
17-
}
18+
}

backend/src/server.js

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { Server } from 'socket.io';
22
import { createClient } from 'redis';
33
import { createServer } from 'http';
4+
import { v4 as uuidv4 } from 'uuid';
45

56
// Redis client configuration
67
const redisClient = createClient({
78
url: 'redis://localhost:6379/0'
89
});
910

10-
// Allowed channels for Redis pub/sub
11-
const allowedChannels = ['Scene:Jack', 'Scene:Jane', 'Human:Jack', 'Jack:Human', 'Agent:Runtime', 'Runtime:Agent'];
11+
// // Allowed channels for Redis pub/sub
12+
// const allowedChannels = ['Scene:Jack', 'Scene:Jane', 'Human:Jack', 'Jack:Human', 'Agent:Runtime', 'Runtime:Agent'];
1213

1314
// Connect Redis client
1415
redisClient.on('error', (err) => {
@@ -37,17 +38,75 @@ const init = async () => {
3738
const subscriber = redisClient.duplicate();
3839
await subscriber.connect();
3940

40-
await subscriber.subscribe(allowedChannels, (message, channel) => {
41-
console.log(`Received message from ${channel}: ${message}`);
42-
io.emit('new_message', { channel, message });
43-
});
41+
// await subscriber.subscribe(allowedChannels, (message, channel) => {
42+
// console.log(`Received message from ${channel}: ${message}`);
43+
// io.emit('new_message', { channel, message });
44+
// });
45+
46+
// Store active sessions and their Redis channels
47+
const activeSessions = {};
48+
49+
const getAllowedChannels = (sessionId, sessionType) => {
50+
if (sessionType === 'Human/AI') {
51+
return [
52+
`Scene:Jack:${sessionId}`,
53+
`Scene:Jane:${sessionId}`,
54+
`Human:Jack:${sessionId}`,
55+
`Jack:Human:${sessionId}`,
56+
`Agent:Runtime:${sessionId}`,
57+
`Runtime:Agent:${sessionId}`,
58+
];
59+
}
60+
return [
61+
`Human:Jack:${sessionId}`,
62+
`Jack:Human:${sessionId}`,
63+
`Agent:Runtime:${sessionId}`,
64+
`Runtime:Agent:${sessionId}`,
65+
];
66+
};
4467

4568
// Socket.IO connection handling
4669
io.on('connection', (socket) => {
70+
4771
console.log('A user connected');
72+
// const socketState = {
73+
// currentSessionId: null
74+
// };
75+
76+
socket.on('create_session', async ({sessionType}, callback) => {
77+
const sessionId = uuidv4();
78+
const channels = getAllowedChannels(sessionId, sessionType)
79+
activeSessions[sessionId] = {channels, sessionType}
80+
81+
console.log(`New session created: ${sessionId}, Type: ${sessionType}`);
82+
83+
await subscriber.subscribe(channels, (message, channels) => {
84+
console.log(`Received message from ${channels}: ${message}`);
85+
io.to(sessionId).emit('new_message', { channels, message });
86+
})
4887

49-
socket.on('chat_message', async (message) => {
50-
console.log('Received chat message:', message);
88+
callback({ sessionId });
89+
90+
socket.join(sessionId);
91+
});
92+
93+
// Join an existing session
94+
socket.on('join_session', async ({ sessionId }, callback) => {
95+
if (!activeSessions[sessionId]) {
96+
callback({ success: false, error: 'Session does not exist' });
97+
return;
98+
}
99+
100+
console.log(`User joined session: ${sessionId}`);
101+
socket.join(sessionId);
102+
// socketState.currentSessionId = sessionId;
103+
callback({ success: true });
104+
});
105+
106+
socket.on('chat_message', async ({ sessionId, message}) => {
107+
if (!sessionId) return;
108+
109+
console.log(`Chat message in session ${sessionId}:`, message);
51110
try {
52111
const agentAction = {
53112
data: {
@@ -58,14 +117,16 @@ const init = async () => {
58117
data_type: "agent_action"
59118
}
60119
};
61-
await redisClient.publish('Human:Jack', JSON.stringify(agentAction));
120+
await redisClient.publish(`Human:Jack:${sessionId}`, JSON.stringify(agentAction));
62121
} catch (err) {
63122
console.error('Error publishing chat message:', err);
64123
}
65124
});
66125

67-
socket.on('save_file', async ({ path, content }) => {
68-
console.log('Saving file:', path);
126+
socket.on('save_file', async ({ sessionId, path, content }) => {
127+
if (!sessionId) return;
128+
129+
console.log(`Saving file in session ${sessionId}:`, path);
69130
try {
70131
const saveMessage = {
71132
data: {
@@ -76,14 +137,16 @@ const init = async () => {
76137
data_type: "agent_action"
77138
}
78139
};
79-
await redisClient.publish('Agent:Runtime', JSON.stringify(saveMessage));
140+
await redisClient.publish(`Agent:Runtime:${sessionId}`, JSON.stringify(saveMessage));
80141
} catch (err) {
81142
console.error('Error publishing save file message:', err);
82143
}
83144
});
84145

85-
socket.on('terminal_command', async (command) => {
86-
console.log('Received terminal command:', command);
146+
socket.on('terminal_command', async ({ sessionId, command }) => {
147+
if (!sessionId) return;
148+
console.log(`Terminal command in session ${sessionId}:`, command);
149+
87150
try {
88151
const messageEnvelope = {
89152
data: {
@@ -94,11 +157,11 @@ const init = async () => {
94157
data_type: "agent_action"
95158
}
96159
};
97-
await redisClient.publish('Agent:Runtime', JSON.stringify(messageEnvelope));
160+
await redisClient.publish(`Agent:Runtime:${sessionId}`, JSON.stringify(messageEnvelope));
98161
} catch (err) {
99162
console.error('Error publishing command:', err);
100163
socket.emit('new_message', {
101-
channel: 'Runtime:Agent',
164+
channel: `Runtime:Agent:${sessionId}`,
102165
message: JSON.stringify({
103166
data: {
104167
data_type: "text",
@@ -110,14 +173,16 @@ const init = async () => {
110173
});
111174

112175
// Handle process initialization
113-
socket.on('init_process', async () => {
114-
console.log('Received init_process request');
176+
socket.on('init_process', async (sessionId) => {
177+
if (!sessionId) return;
178+
179+
console.log(`Initializing process in session ${sessionId}`);
115180
try {
116181
const initParams = {
117182
node_name: "openhands_node",
118-
input_channels: ["Agent:Runtime"],
119-
output_channels: ["Runtime:Agent"],
120-
modal_session_id: "arpan"
183+
input_channels: [`Agent:Runtime:${sessionId}`],
184+
output_channels: [`Runtime:Agent:${sessionId}`],
185+
modal_session_id: sessionId
121186
};
122187

123188
const response = await fetch('http://localhost:5000/initialize', {
@@ -134,7 +199,8 @@ const init = async () => {
134199
const result = await response.json();
135200

136201
if (result.status === 'initialized') {
137-
socket.emit('init_process_result', { success: true });
202+
socket.emit('init_process_result', { success: true, sessionId: sessionId });
203+
// callback({ success: true });
138204
console.log('OpenHands initialized successfully');
139205
} else {
140206
throw new Error(`Unexpected initialization status: ${result.status}`);
@@ -145,9 +211,27 @@ const init = async () => {
145211
success: false,
146212
error: err.message
147213
});
214+
// callback({ success: false, error: err.message });
148215
}
149216
});
150217

218+
// Stop/Kill a session
219+
socket.on('kill_session', async ( { sessionId }, callback ) => {
220+
if (!activeSessions[sessionId]) {
221+
callback({ success: false, error: 'Session does not exist' });
222+
return;
223+
}
224+
225+
console.log(`Killing session: ${sessionId}`);
226+
const { channels } = activeSessions[sessionId];
227+
await subscriber.unsubscribe(channels);
228+
229+
io.to(sessionId).emit('session_terminated');
230+
delete activeSessions[sessionId];
231+
232+
callback({ success: true });
233+
});
234+
151235
socket.on('disconnect', () => {
152236
console.log('A user disconnected');
153237
});

frontend/app/page.tsx

Lines changed: 117 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,68 @@ import { useRouter } from "next/navigation";
55
import { useLoading } from "../context/loading-provider";
66
import Loading from "./loading";
77
import Error from "./error";
8-
import { useState } from "react";
8+
import { useEffect, useState } from "react";
99
import { Sparkles, Code2 } from 'lucide-react';
1010
import { motion } from 'framer-motion';
11+
import {
12+
Popover,
13+
PopoverContent,
14+
PopoverTrigger,
15+
} from "@/components/ui/popover"
16+
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
1117

1218
export default function LandingPage() {
19+
const sessionOptions = [
20+
{ value: "Human/Human", label: "Human/ Human" },
21+
{ value: "Human/AI", label: "Human/ AI" },
22+
];
23+
1324
const router = useRouter();
1425
const { isReady, error, socket } = useLoading();
1526
const [isInitializing, setIsInitializing] = useState(false);
27+
const [sessionType, setSessionType] = useState<'Human/AI' | 'Human/Human'>('Human/AI');
28+
const [sessionIdInput, setSessionIdInput] = useState('');
1629

1730
const handleStartSession = () => {
18-
console.log('Start Session button clicked');
31+
console.log('Start Session');
32+
localStorage.removeItem("cotomata-sessionId");
1933
setIsInitializing(true);
2034
if (socket) {
2135
console.log('Emitting init_process event');
22-
socket.emit('init_process');
36+
socket.emit('create_session', { sessionType }, (response: any) => {
37+
socket.emit('init_process', response.sessionId );
38+
});
2339
} else {
2440
console.error('Socket not available');
2541
setIsInitializing(false);
2642
}
2743
};
2844

45+
const handleJoinSession = () => {
46+
console.log('Join Existing Session');
47+
localStorage.removeItem("cotomata-sessionId");
48+
setIsInitializing(true);
49+
if (socket) {
50+
socket.emit('join_session', { sessionId: sessionIdInput }, (response: any) => {
51+
if (response.success) {
52+
socket.emit('init_process', sessionIdInput );
53+
}
54+
});
55+
}
56+
};
57+
2958
if (error) {
3059
return <Error error={error} reset={() => router.refresh()} />;
3160
}
3261

33-
if (isReady) {
34-
router.push('/workspace');
35-
return null;
36-
}
62+
useEffect(() => {
63+
if (isReady) {
64+
const sessionId = localStorage.getItem("cotomata-sessionId");
65+
if (sessionId) {
66+
router.push(`/workspace/${sessionId}`);
67+
}
68+
}
69+
}, [isReady, router]);
3770

3871
return (
3972
<div className="flex min-h-screen flex-col bg-[#0A0A0A]">
@@ -161,19 +194,84 @@ export default function LandingPage() {
161194
<Loading />
162195
</div>
163196
) : (
164-
<motion.div
165-
whileHover={{ scale: 1.03 }}
166-
whileTap={{ scale: 0.98 }}
167-
>
168-
<Button
169-
size="lg"
170-
onClick={handleStartSession}
171-
className="w-full h-16 bg-white hover:bg-gray-100 text-black text-xl font-medium rounded-2xl shadow-lg"
197+
<div className="flex flex-row gap-5 justify-center">
198+
<motion.div
199+
whileHover={{ scale: 1.03 }}
200+
whileTap={{ scale: 0.98 }}
172201
>
173-
<Code2 className="mr-3 h-6 w-6" />
174-
Start Session
175-
</Button>
176-
</motion.div>
202+
<Popover>
203+
<PopoverTrigger asChild>
204+
<Button
205+
size="lg"
206+
className="w-full h-16 bg-white hover:bg-gray-100 text-black text-xl font-medium rounded-2xl shadow-lg"
207+
>
208+
<Code2 className="mr-3 h-6 w-6" />
209+
Start New Session
210+
</Button>
211+
</PopoverTrigger>
212+
213+
<PopoverContent>
214+
<div className="relative flex flex-col gap-5 rounded-lg items-center">
215+
<Select value={sessionType} onValueChange={(value) => setSessionType(value as "Human/AI" | "Human/Human")}>
216+
<SelectTrigger className="w-[180px]">
217+
<SelectValue/>
218+
</SelectTrigger>
219+
<SelectContent>
220+
<SelectGroup>
221+
{sessionOptions.map((option) => (
222+
<SelectItem key={option.value} value={option.value}>
223+
{option.label}
224+
</SelectItem>
225+
))}
226+
</SelectGroup>
227+
</SelectContent>
228+
</Select>
229+
<Button
230+
onClick={handleStartSession}
231+
className="mt-4 text-black bg-white hover:bg-gray-100 font-medium"
232+
>
233+
Start Chat
234+
</Button>
235+
</div>
236+
</PopoverContent>
237+
</Popover>
238+
</motion.div>
239+
240+
<motion.div
241+
whileHover={{ scale: 1.03 }}
242+
whileTap={{ scale: 0.98 }}
243+
>
244+
<Popover>
245+
<PopoverTrigger asChild>
246+
<Button
247+
size="lg"
248+
className="w-full h-16 bg-white hover:bg-gray-100 text-black text-xl font-medium rounded-2xl shadow-lg"
249+
>
250+
<Code2 className="mr-3 h-6 w-6" />
251+
Join Existing Session
252+
</Button>
253+
</PopoverTrigger>
254+
255+
<PopoverContent>
256+
<div className="relative flex flex-col gap-5 rounded-lg items-center">
257+
<input
258+
type="text"
259+
placeholder="Enter Session ID"
260+
value={sessionIdInput}
261+
onChange={(e) => setSessionIdInput(e.target.value)}
262+
className="w-full mt-4 px-4 py-2 bg-gray-800 text-white rounded-lg outline-none"
263+
/>
264+
<Button
265+
onClick={handleJoinSession}
266+
className="mt-4 text-black bg-white hover:bg-gray-100 font-medium"
267+
>
268+
Join Session
269+
</Button>
270+
</div>
271+
</PopoverContent>
272+
</Popover>
273+
</motion.div>
274+
</div>
177275
)}
178276
</motion.div>
179277
</div>

0 commit comments

Comments
 (0)