@aion0/forge 0.5.36 → 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.
|
|
1
|
+
# Forge v0.5.38
|
|
2
2
|
|
|
3
|
-
Released: 2026-04-
|
|
3
|
+
Released: 2026-04-14
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.37
|
|
6
6
|
|
|
7
|
-
### Bug Fixes
|
|
8
|
-
- fix: workspace smith bell always sends to Telegram
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.35...v0.5.36
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -63,6 +63,7 @@ export class AgentBus extends EventEmitter {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
this.log.push(msg);
|
|
66
|
+
this.pruneIfNeeded();
|
|
66
67
|
this.emit('message', msg);
|
|
67
68
|
|
|
68
69
|
// ACK messages don't need delivery tracking
|
|
@@ -350,6 +351,35 @@ export class AgentBus extends EventEmitter {
|
|
|
350
351
|
if (count > 0) console.log(`[bus] Marked ${count} pending messages as failed (restart cleanup)`);
|
|
351
352
|
}
|
|
352
353
|
|
|
354
|
+
// ─── Log maintenance ───────────────────────────────────
|
|
355
|
+
|
|
356
|
+
private static readonly MAX_LOG_SIZE = 500;
|
|
357
|
+
private static readonly PRUNE_KEEP = 200;
|
|
358
|
+
|
|
359
|
+
/** Prune completed messages to prevent unbounded log growth.
|
|
360
|
+
* Keeps: all pending/running messages + last N completed ones.
|
|
361
|
+
* Called automatically after each send(). */
|
|
362
|
+
private pruneIfNeeded(): void {
|
|
363
|
+
if (this.log.length <= AgentBus.MAX_LOG_SIZE) return;
|
|
364
|
+
|
|
365
|
+
// Separate active (must keep) from completed (can prune)
|
|
366
|
+
const active: BusMessage[] = [];
|
|
367
|
+
const completed: BusMessage[] = [];
|
|
368
|
+
for (const m of this.log) {
|
|
369
|
+
if (m.status === 'pending' || m.status === 'pending_approval' || m.status === 'running') {
|
|
370
|
+
active.push(m);
|
|
371
|
+
} else {
|
|
372
|
+
completed.push(m);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Keep all active + most recent N completed
|
|
377
|
+
const keep = completed.slice(-AgentBus.PRUNE_KEEP);
|
|
378
|
+
this.log = [...active, ...keep].sort((a, b) => a.timestamp - b.timestamp);
|
|
379
|
+
const pruned = completed.length - keep.length;
|
|
380
|
+
if (pruned > 0) console.log(`[bus] Pruned ${pruned} completed messages (${this.log.length} remaining)`);
|
|
381
|
+
}
|
|
382
|
+
|
|
353
383
|
// ─── Query ─────────────────────────────────────────────
|
|
354
384
|
|
|
355
385
|
getMessagesFor(agentId: string): BusMessage[] {
|
|
@@ -374,6 +404,11 @@ export class AgentBus extends EventEmitter {
|
|
|
374
404
|
return this.log;
|
|
375
405
|
}
|
|
376
406
|
|
|
407
|
+
/** Get log for persistence — excludes _system messages (transient notifications) */
|
|
408
|
+
getLogForPersistence(): BusMessage[] {
|
|
409
|
+
return this.log.filter(m => m.to !== '_system');
|
|
410
|
+
}
|
|
411
|
+
|
|
377
412
|
/** Get all outbox queues (for persistence) */
|
|
378
413
|
getAllOutbox(): Record<string, BusMessage[]> {
|
|
379
414
|
const result: Record<string, BusMessage[]> = {};
|
|
@@ -385,6 +420,7 @@ export class AgentBus extends EventEmitter {
|
|
|
385
420
|
|
|
386
421
|
loadLog(messages: BusMessage[]): void {
|
|
387
422
|
this.log = [...messages];
|
|
423
|
+
this.pruneIfNeeded();
|
|
388
424
|
}
|
|
389
425
|
|
|
390
426
|
/** Restore outbox from persisted state */
|
|
@@ -1870,7 +1870,7 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1870
1870
|
agents: Array.from(this.agents.values()).map(e => e.config),
|
|
1871
1871
|
agentStates: this.getAllAgentStates(),
|
|
1872
1872
|
nodePositions: this.nodePositions,
|
|
1873
|
-
busLog:
|
|
1873
|
+
busLog: this.bus.getLogForPersistence(),
|
|
1874
1874
|
busOutbox: this.bus.getAllOutbox(),
|
|
1875
1875
|
createdAt: this.createdAt,
|
|
1876
1876
|
updatedAt: Date.now(),
|
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