the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add optional session key for shared PTY sessions

Introduce an optional key parameter for session creation and lookup,
defaulting to the sandbox id. Derive the key from the sessionId URL
parameter so shared connections can reuse the same PTY session. Update
all PTY backends and session maps to set/delete by key and propagate it
through getSession/createTerminalSession calls

+30 -24
+2 -2
apps/api/src/pty/e2b/index.ts
··· 6 6 import decrypt from "lib/decrypt"; 7 7 import type { Message } from "pty/pty-tunnel/messages"; 8 8 9 - export async function createTerminalSession(ctx: Context, id: string): Promise<Session> { 9 + export async function createTerminalSession(ctx: Context, id: string, key = id): Promise<Session> { 10 10 const [record] = await ctx.db 11 11 .select() 12 12 .from(schema.sandboxes) ··· 65 65 pid = terminal.pid; 66 66 consola.info("E2B PTY session created", { id, pid }); 67 67 68 - ctx.sessions.set(id, session); 68 + ctx.sessions.set(key, session); 69 69 return session; 70 70 }
+12 -9
apps/api/src/pty/index.ts
··· 38 38 next(); 39 39 }); 40 40 41 - async function getSession(ctx: Context, id: string) { 42 - if (ctx.sessions.has(id)) { 43 - const existing = ctx.sessions.get(id)!; 41 + async function getSession(ctx: Context, id: string, key = id) { 42 + if (ctx.sessions.has(key)) { 43 + const existing = ctx.sessions.get(key)!; 44 44 // If the underlying pty-tunnel socket is closed, evict and recreate. 45 45 const sock = existing.socket as { readyState?: number }; 46 46 if (sock.readyState !== undefined && sock.readyState !== 1 /* OPEN */) { 47 - consola.info("PTY session stale, recreating", { id }); 48 - ctx.sessions.delete(id); 47 + consola.info("PTY session stale, recreating", { id, key }); 48 + ctx.sessions.delete(key); 49 49 } else { 50 50 return existing; 51 51 } ··· 65 65 .where(or(eq(schema.sandboxes.id, id), eq(schema.sandboxes.sandboxId, id))) 66 66 .execute(); 67 67 68 - if (record?.modalAuth) return modal.createTerminalSession(ctx, id); 69 - if (record?.e2bAuth) return e2b.createTerminalSession(ctx, id); 70 - return vercel.createTerminalSession(ctx, id); 68 + if (record?.modalAuth) return modal.createTerminalSession(ctx, id, key); 69 + if (record?.e2bAuth) return e2b.createTerminalSession(ctx, id, key); 70 + return vercel.createTerminalSession(ctx, id, key); 71 71 } 72 72 73 73 router.get("/:id/stream", async (req, res) => { ··· 145 145 } 146 146 } 147 147 148 + const shareId = url.searchParams.get("sessionId") ?? undefined; 149 + const key = shareId ?? id; 150 + 148 151 // The WS upgrade completes immediately but session creation is async. 149 152 // Buffer any messages (resize, keystrokes) that arrive before the session 150 153 // is ready so they can be replayed once the session exists. ··· 154 157 155 158 let session: Awaited<ReturnType<typeof getSession>>; 156 159 try { 157 - session = await getSession(context.ctx, id); 160 + session = await getSession(context.ctx, id, key); 158 161 } catch (err) { 159 162 consola.error("WS: Failed to get session:", err); 160 163 ws.close(1011, "Session error");
+3 -3
apps/api/src/pty/modal/index.ts
··· 97 97 return { sandbox, cmd }; 98 98 } 99 99 100 - export async function createTerminalSession(ctx: Context, id: string) { 100 + export async function createTerminalSession(ctx: Context, id: string, key = id) { 101 101 const [record] = await ctx.db 102 102 .select() 103 103 .from(schema.sandboxes) ··· 188 188 }); 189 189 190 190 socket.addEventListener("close", () => { 191 - ctx.sessions.delete(id); 191 + ctx.sessions.delete(key); 192 192 for (const ws of session.wsClients) { 193 193 if (ws.readyState === ws.OPEN) ws.close(1000, "exit"); 194 194 } ··· 201 201 consola.info("Modal: pty-tunnel socket open, sending ready", chalk.greenBright(id)); 202 202 socket.sendMessage({ type: "ready" }); 203 203 204 - ctx.sessions.set(id, session); 204 + ctx.sessions.set(key, session); 205 205 return session; 206 206 }
+3 -3
apps/api/src/pty/vercel/index.ts
··· 108 108 return { sandbox, cmd }; 109 109 } 110 110 111 - export async function createTerminalSession(ctx: Context, id: string) { 111 + export async function createTerminalSession(ctx: Context, id: string, key = id) { 112 112 const [record] = await ctx.db 113 113 .select() 114 114 .from(schema.sandboxes) ··· 180 180 }); 181 181 182 182 socket.addEventListener("close", () => { 183 - ctx.sessions.delete(id); 183 + ctx.sessions.delete(key); 184 184 for (const ws of session.wsClients) { 185 185 if (ws.readyState === ws.OPEN) ws.close(1000, "exit"); 186 186 } ··· 191 191 await socket.waitForOpen(); 192 192 socket.sendMessage({ type: "ready" }); 193 193 194 - ctx.sessions.set(id, session); 194 + ctx.sessions.set(key, session); 195 195 return session; 196 196 }
+10 -7
apps/api/src/tty/index.tsx
··· 46 46 47 47 const sessions = new Map<string, Session>(); 48 48 49 - async function createTerminalSession(ctx: Context, id: string) { 49 + async function createTerminalSession(ctx: Context, id: string, key = id) { 50 50 const [sandbox] = await ctx.db 51 51 .select() 52 52 .from(schema.sandboxes) ··· 306 306 } 307 307 session.clients.clear(); 308 308 session.wsClients.clear(); 309 - sessions.delete(id); 309 + sessions.delete(key); 310 310 }); 311 311 312 312 cmd.on?.("error", (err: Error) => { ··· 319 319 } 320 320 session.clients.clear(); 321 321 session.wsClients.clear(); 322 - sessions.delete(id); 322 + sessions.delete(key); 323 323 }); 324 324 325 - sessions.set(id, session); 325 + sessions.set(key, session); 326 326 return session; 327 327 } 328 328 329 - async function getSession(ctx: Context, id: string) { 330 - return sessions.get(id) ?? (await createTerminalSession(ctx, id)); 329 + async function getSession(ctx: Context, id: string, key = id) { 330 + return sessions.get(key) ?? (await createTerminalSession(ctx, id, key)); 331 331 } 332 332 333 333 router.get("/:id/stream", async (req, res) => { ··· 398 398 } 399 399 } 400 400 401 + const shareId = url.searchParams.get("sessionId") ?? undefined; 402 + const key = shareId ?? id; 403 + 401 404 // Buffer messages that arrive before the session is ready. 402 405 const pendingMessages: Buffer[] = []; 403 406 const bufferMessage = (data: Buffer) => pendingMessages.push(data); ··· 405 408 406 409 let session: Session; 407 410 try { 408 - session = await getSession(context.ctx, id); 411 + session = await getSession(context.ctx, id, key); 409 412 } catch (err) { 410 413 consola.error("WS: Failed to get session:", err); 411 414 ws.close(1011, "Session error");