@aion0/forge 0.5.37 → 0.5.38

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.
package/RELEASE_NOTES.md CHANGED
@@ -1,11 +1,8 @@
1
- # Forge v0.5.37
1
+ # Forge v0.5.38
2
2
 
3
- Released: 2026-04-11
3
+ Released: 2026-04-14
4
4
 
5
- ## Changes since v0.5.36
5
+ ## Changes since v0.5.37
6
6
 
7
- ### Performance
8
- - perf: prune bus log to prevent unbounded growth
9
7
 
10
-
11
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.36...v0.5.37
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.37...v0.5.38
@@ -1446,6 +1446,8 @@ const MemoTerminalPane = memo(function TerminalPane({
1446
1446
  const MAX_CREATE_RETRIES = 2;
1447
1447
  let reconnectAttempts = 0;
1448
1448
  let isNewlyCreated = false;
1449
+ let lastActivityTime = Date.now();
1450
+ let staleCheckTimer = 0;
1449
1451
 
1450
1452
  function connect() {
1451
1453
  if (disposed) return;
@@ -1455,6 +1457,7 @@ const MemoTerminalPane = memo(function TerminalPane({
1455
1457
  socket.onopen = () => {
1456
1458
  if (disposed) { socket.close(); return; }
1457
1459
  if (socket.readyState !== WebSocket.OPEN) return;
1460
+ lastActivityTime = Date.now();
1458
1461
  const cols = term.cols;
1459
1462
  const rows = term.rows;
1460
1463
 
@@ -1477,6 +1480,7 @@ const MemoTerminalPane = memo(function TerminalPane({
1477
1480
 
1478
1481
  ws.onmessage = (event) => {
1479
1482
  if (disposed || !initDone) return;
1483
+ lastActivityTime = Date.now();
1480
1484
  try {
1481
1485
  const msg = JSON.parse(event.data);
1482
1486
  if (msg.type === 'output') {
@@ -1573,6 +1577,36 @@ const MemoTerminalPane = memo(function TerminalPane({
1573
1577
  };
1574
1578
  }
1575
1579
 
1580
+ // Stale detection: WebSocket may appear "open" after Mac sleep but have no traffic.
1581
+ // If no activity for 60s, force-close and reconnect.
1582
+ staleCheckTimer = window.setInterval(() => {
1583
+ if (disposed) return;
1584
+ if (ws?.readyState === WebSocket.OPEN && Date.now() - lastActivityTime > 60000) {
1585
+ console.warn('[ws] terminal stream stalled, forcing reconnect');
1586
+ lastActivityTime = Date.now();
1587
+ try { ws.close(); } catch {}
1588
+ }
1589
+ }, 20000);
1590
+
1591
+ // Detect tab wake from sleep — only triggers when tab was actually hidden.
1592
+ // Conservative: needs > 60s of inactivity to force reconnect.
1593
+ const onVisibilityChange = () => {
1594
+ if (disposed) return;
1595
+ if (document.visibilityState === 'visible' && Date.now() - lastActivityTime > 60000) {
1596
+ console.log('[ws] terminal visible after long idle, forcing reconnect');
1597
+ lastActivityTime = Date.now();
1598
+ try { ws?.close(); } catch {}
1599
+ }
1600
+ };
1601
+ const onOnline = () => {
1602
+ if (disposed) return;
1603
+ console.log('[ws] network online, reconnecting terminal');
1604
+ lastActivityTime = Date.now();
1605
+ try { ws?.close(); } catch {}
1606
+ };
1607
+ document.addEventListener('visibilitychange', onVisibilityChange);
1608
+ window.addEventListener('online', onOnline);
1609
+
1576
1610
  // NOTE: connect() is called inside initTerminal() — do NOT call it here.
1577
1611
  // Calling it both here and in initTerminal() causes duplicate WebSocket
1578
1612
  // connections to the same tmux session, resulting in doubled output.
@@ -1683,6 +1717,9 @@ const MemoTerminalPane = memo(function TerminalPane({
1683
1717
  visObserver.disconnect();
1684
1718
  clearTimeout(resizeTimer);
1685
1719
  clearTimeout(reconnectTimer);
1720
+ if (staleCheckTimer) clearInterval(staleCheckTimer);
1721
+ document.removeEventListener('visibilitychange', onVisibilityChange);
1722
+ window.removeEventListener('online', onOnline);
1686
1723
  window.removeEventListener('terminal-drag-end', onDragEnd);
1687
1724
  resizeObserver.disconnect();
1688
1725
  // Strict Mode cleanup: if disposed within 2s of mount and we created a
@@ -280,9 +280,83 @@ function useWorkspaceStream(workspaceId: string | null, onEvent?: (event: any) =
280
280
  useEffect(() => {
281
281
  if (!workspaceId) return;
282
282
 
283
- const es = new EventSource(`/api/workspace/${workspaceId}/stream`);
283
+ // Reconnection state survives sleep/wake and network drops
284
+ let es: EventSource | null = null;
285
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
286
+ let staleCheckTimer: ReturnType<typeof setInterval> | null = null;
287
+ let reconnectAttempts = 0;
288
+ let lastEventTime = Date.now();
289
+ let disposed = false;
290
+
291
+ const scheduleReconnect = () => {
292
+ if (disposed) return;
293
+ if (reconnectTimer) return;
294
+ // Exponential backoff: 2s, 4s, 8s, max 30s
295
+ const delay = Math.min(30000, 2000 * Math.pow(2, reconnectAttempts));
296
+ reconnectAttempts++;
297
+ console.log(`[sse] reconnecting in ${delay / 1000}s (attempt ${reconnectAttempts})`);
298
+ reconnectTimer = setTimeout(() => {
299
+ reconnectTimer = null;
300
+ connect();
301
+ }, delay);
302
+ };
303
+
304
+ const connect = () => {
305
+ if (disposed) return;
306
+ try { es?.close(); } catch {}
307
+ lastEventTime = Date.now();
308
+ es = new EventSource(`/api/workspace/${workspaceId}/stream`);
309
+
310
+ es.onopen = () => {
311
+ reconnectAttempts = 0;
312
+ lastEventTime = Date.now();
313
+ };
314
+
315
+ es.onerror = () => {
316
+ // Browser fires onerror when the connection drops
317
+ console.warn('[sse] EventSource error — will reconnect');
318
+ try { es?.close(); } catch {}
319
+ scheduleReconnect();
320
+ };
321
+
322
+ es.onmessage = (e) => {
323
+ lastEventTime = Date.now();
324
+ handleEvent(e);
325
+ };
326
+ };
327
+
328
+ // Stall detection: if we haven't received any event (including heartbeat ping from server)
329
+ // for 45s, the connection is stuck (common after Mac sleep). Force reconnect.
330
+ staleCheckTimer = setInterval(() => {
331
+ if (disposed) return;
332
+ if (Date.now() - lastEventTime > 45000) {
333
+ console.warn('[sse] stream stalled (no events for 45s), forcing reconnect');
334
+ try { es?.close(); } catch {}
335
+ lastEventTime = Date.now(); // prevent immediate re-trigger
336
+ connect();
337
+ }
338
+ }, 15000);
339
+
340
+ // Detect tab wake from sleep / network recovery — conservative to avoid churn
341
+ const onVisibilityChange = () => {
342
+ if (disposed) return;
343
+ if (document.visibilityState === 'visible' && Date.now() - lastEventTime > 60000) {
344
+ console.log('[sse] page visible after long idle, reconnecting');
345
+ try { es?.close(); } catch {}
346
+ connect();
347
+ }
348
+ };
349
+ const onOnline = () => {
350
+ if (disposed) return;
351
+ console.log('[sse] network online, reconnecting');
352
+ try { es?.close(); } catch {}
353
+ lastEventTime = Date.now();
354
+ connect();
355
+ };
356
+ document.addEventListener('visibilitychange', onVisibilityChange);
357
+ window.addEventListener('online', onOnline);
284
358
 
285
- es.onmessage = (e) => {
359
+ const handleEvent = (e: MessageEvent) => {
286
360
  try {
287
361
  const event = JSON.parse(e.data);
288
362
 
@@ -408,7 +482,17 @@ function useWorkspaceStream(workspaceId: string | null, onEvent?: (event: any) =
408
482
  } catch {}
409
483
  };
410
484
 
411
- return () => es.close();
485
+ // Start the initial connection
486
+ connect();
487
+
488
+ return () => {
489
+ disposed = true;
490
+ if (reconnectTimer) clearTimeout(reconnectTimer);
491
+ if (staleCheckTimer) clearInterval(staleCheckTimer);
492
+ document.removeEventListener('visibilitychange', onVisibilityChange);
493
+ window.removeEventListener('online', onOnline);
494
+ try { es?.close(); } catch {}
495
+ };
412
496
  }, [workspaceId]);
413
497
 
414
498
  return { agents, states, logPreview, busLog, setAgents, daemonActive, setDaemonActive };
package/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.37",
3
+ "version": "0.5.38",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {