osborn 0.8.8 → 0.8.9

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.
Files changed (2) hide show
  1. package/dist/index.js +41 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -227,6 +227,47 @@ function startApiServer(workingDir, port) {
227
227
  setTimeout(() => process.exit(0), 150);
228
228
  return;
229
229
  }
230
+ // GET /events — Server-Sent Events heartbeat for cloud-sandbox keepalive.
231
+ //
232
+ // This endpoint is the single thing preventing Sprites' CRIU-based
233
+ // hibernation from freezing osborn's Node.js event loop and dropping our
234
+ // LiveKit WebSocket mid-session. Short HTTP pings don't work: Sprites'
235
+ // warm state serves /health responses from a process snapshot without
236
+ // actually resuming the event loop, so background timers (including
237
+ // LiveKit heartbeats) stop firing after a few seconds. That causes the
238
+ // LiveKit server to drop osborn's participant, delete the room, and
239
+ // leave any future user joins stuck at "Connecting..." forever.
240
+ //
241
+ // An OPEN long-lived TCP connection keeps the sprite in 'running' state.
242
+ // The frontend opens this endpoint on chat page mount and holds it open
243
+ // for the entire voice session. While open, osborn's event loop ticks
244
+ // continuously, LiveKit heartbeats fire, and the room stays alive.
245
+ //
246
+ // For local (non-cloud) dev, this endpoint is harmless — it just idles
247
+ // on a client that may never connect. Zero cost when unused.
248
+ if (req.method === 'GET' && url.pathname === '/events') {
249
+ res.writeHead(200, {
250
+ 'Content-Type': 'text/event-stream',
251
+ 'Cache-Control': 'no-cache',
252
+ 'Connection': 'keep-alive',
253
+ // Disable proxy buffering (nginx-style) so each ping is flushed
254
+ // through Sprites' reverse proxy immediately rather than batched.
255
+ 'X-Accel-Buffering': 'no',
256
+ });
257
+ res.write(`: sprite-keepalive connected at ${new Date().toISOString()}\n\n`);
258
+ const heartbeat = setInterval(() => {
259
+ try {
260
+ res.write(`: ping ${Date.now()}\n\n`);
261
+ }
262
+ catch { }
263
+ }, 10_000);
264
+ req.on('close', () => {
265
+ clearInterval(heartbeat);
266
+ console.log('[events] SSE client disconnected');
267
+ });
268
+ console.log('[events] SSE client connected');
269
+ return;
270
+ }
230
271
  res.writeHead(404, { 'Content-Type': 'application/json' });
231
272
  res.end(JSON.stringify({ error: 'Not found' }));
232
273
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {