@rajat-rastogi/maestro 0.5.5 → 0.6.0

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.
@@ -580,6 +580,29 @@ class AgentConnectionPool {
580
580
  entry.ready = true;
581
581
  entry.queue.forEach((cb) => cb(ws));
582
582
  entry.queue = [];
583
+ entry.heartbeat = setInterval(() => {
584
+ if (ws.readyState !== WebSocket.OPEN) return;
585
+ let gotPong = false;
586
+ const handler = (data) => {
587
+ try {
588
+ const msg = JSON.parse(data.toString());
589
+ if (msg.type === "pong") {
590
+ gotPong = true;
591
+ ws.removeListener("message", handler);
592
+ }
593
+ } catch {
594
+ }
595
+ };
596
+ ws.on("message", handler);
597
+ ws.send(JSON.stringify({ type: "ping" }));
598
+ setTimeout(() => {
599
+ ws.removeListener("message", handler);
600
+ if (!gotPong && ws.readyState === WebSocket.OPEN) {
601
+ console.log(`[AgentPool] heartbeat failed for "${tunnelId}" — zombie connection, closing`);
602
+ ws.terminate();
603
+ }
604
+ }, 1e4);
605
+ }, 6e4);
583
606
  resolve(ws);
584
607
  });
585
608
  ws.on("error", (err) => {
@@ -685,6 +708,7 @@ class AgentConnectionPool {
685
708
  teardown(tunnelId) {
686
709
  const entry = this.pool.get(tunnelId);
687
710
  if (entry) {
711
+ if (entry.heartbeat) clearInterval(entry.heartbeat);
688
712
  try {
689
713
  entry.ws?.close();
690
714
  } catch {
@@ -692,6 +716,9 @@ class AgentConnectionPool {
692
716
  this.pool.delete(tunnelId);
693
717
  }
694
718
  }
719
+ releaseOne(tunnelId) {
720
+ this.teardown(tunnelId);
721
+ }
695
722
  releaseAll() {
696
723
  for (const id of Array.from(this.pool.keys())) {
697
724
  this.teardown(id);
@@ -1373,7 +1400,11 @@ class AgentPtyAdapter {
1373
1400
  dataCallback = null;
1374
1401
  exitCallback = null;
1375
1402
  messageHandler = null;
1403
+ dead = false;
1376
1404
  async connect(cwd, cols, rows) {
1405
+ this.lastCwd = cwd;
1406
+ this.lastCols = cols;
1407
+ this.lastRows = rows;
1377
1408
  this.ws = await AgentConnectionPool.getInstance().getConnection(this.tunnelId);
1378
1409
  this.messageHandler = (raw) => {
1379
1410
  try {
@@ -1388,6 +1419,13 @@ class AgentPtyAdapter {
1388
1419
  }
1389
1420
  };
1390
1421
  this.ws.on("message", this.messageHandler);
1422
+ this.ws.on("close", () => {
1423
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket closed`);
1424
+ this.handleConnectionLost();
1425
+ });
1426
+ this.ws.on("error", (err) => {
1427
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket error: ${err.message}`);
1428
+ });
1391
1429
  this.ws.send(JSON.stringify({ type: "create", id: this.sessionId, shell: "powershell", cwd, cols, rows }));
1392
1430
  await new Promise((resolve) => {
1393
1431
  const t = setTimeout(resolve, 5e3);
@@ -1405,6 +1443,61 @@ class AgentPtyAdapter {
1405
1443
  this.ws.on("message", ackHandler);
1406
1444
  });
1407
1445
  }
1446
+ reconnectAttempts = 0;
1447
+ lastCwd = "";
1448
+ lastCols = 120;
1449
+ lastRows = 30;
1450
+ async handleConnectionLost() {
1451
+ if (this.dead) return;
1452
+ if (this.reconnectAttempts < 3) {
1453
+ this.reconnectAttempts++;
1454
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnecting (attempt ${this.reconnectAttempts}/3)...`);
1455
+ if (this.dataCallback) {
1456
+ this.dataCallback(`\r
1457
+ \x1B[1;33m[Maestro] Connection lost — reconnecting (${this.reconnectAttempts}/3)...\x1B[0m\r
1458
+ `);
1459
+ }
1460
+ AgentConnectionPool.getInstance().releaseOne(this.tunnelId);
1461
+ await new Promise((r) => setTimeout(r, 2e3 * this.reconnectAttempts));
1462
+ try {
1463
+ this.ws = await AgentConnectionPool.getInstance().getConnection(this.tunnelId);
1464
+ if (this.messageHandler) {
1465
+ this.ws.on("message", this.messageHandler);
1466
+ }
1467
+ this.ws.on("close", () => {
1468
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket closed`);
1469
+ this.handleConnectionLost();
1470
+ });
1471
+ this.ws.on("error", (err) => {
1472
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} WebSocket error: ${err.message}`);
1473
+ });
1474
+ this.ws.send(JSON.stringify({
1475
+ type: "create",
1476
+ id: this.sessionId,
1477
+ shell: "powershell",
1478
+ cwd: this.lastCwd,
1479
+ cols: this.lastCols,
1480
+ rows: this.lastRows
1481
+ }));
1482
+ this.reconnectAttempts = 0;
1483
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnected successfully`);
1484
+ if (this.dataCallback) {
1485
+ this.dataCallback("\r\n\x1B[1;32m[Maestro] Reconnected to remote agent.\x1B[0m\r\n");
1486
+ }
1487
+ return;
1488
+ } catch (err) {
1489
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
1490
+ }
1491
+ }
1492
+ this.dead = true;
1493
+ console.log(`[AgentPty] ${this.sessionId.slice(0, 8)} all reconnect attempts failed`);
1494
+ if (this.dataCallback) {
1495
+ this.dataCallback("\r\n\x1B[1;31m[Maestro] Connection to remote agent lost. Close and recreate the terminal.\x1B[0m\r\n");
1496
+ }
1497
+ if (this.exitCallback) {
1498
+ this.exitCallback(null);
1499
+ }
1500
+ }
1408
1501
  onData(cb) {
1409
1502
  this.dataCallback = cb;
1410
1503
  }
@@ -1412,16 +1505,25 @@ class AgentPtyAdapter {
1412
1505
  this.exitCallback = cb;
1413
1506
  }
1414
1507
  write(data) {
1415
- this.ws?.send(JSON.stringify({ type: "input", id: this.sessionId, data }));
1508
+ if (this.ws?.readyState === WebSocket.OPEN) {
1509
+ this.ws.send(JSON.stringify({ type: "input", id: this.sessionId, data }));
1510
+ }
1416
1511
  }
1417
1512
  resize(cols, rows) {
1418
- this.ws?.send(JSON.stringify({ type: "resize", id: this.sessionId, cols, rows }));
1513
+ this.lastCols = cols;
1514
+ this.lastRows = rows;
1515
+ if (this.ws?.readyState === WebSocket.OPEN) {
1516
+ this.ws.send(JSON.stringify({ type: "resize", id: this.sessionId, cols, rows }));
1517
+ }
1419
1518
  }
1420
1519
  kill() {
1520
+ this.dead = true;
1421
1521
  if (this.ws && this.messageHandler) {
1422
1522
  this.ws.removeListener("message", this.messageHandler);
1423
1523
  }
1424
- this.ws?.send(JSON.stringify({ type: "destroy", id: this.sessionId }));
1524
+ if (this.ws?.readyState === WebSocket.OPEN) {
1525
+ this.ws.send(JSON.stringify({ type: "destroy", id: this.sessionId }));
1526
+ }
1425
1527
  }
1426
1528
  autoLaunch(provider) {
1427
1529
  if (provider === "claude") {
@@ -1868,6 +1970,32 @@ class SessionManager {
1868
1970
  this.push("sessions:stateChanged", ev);
1869
1971
  }
1870
1972
  }
1973
+ let logStream = null;
1974
+ try {
1975
+ const logDir = electron.app.getPath("userData");
1976
+ fs__namespace.mkdirSync(logDir, { recursive: true });
1977
+ const logFile = path.join(logDir, "maestro-gui.log");
1978
+ try {
1979
+ if (fs__namespace.existsSync(logFile)) fs__namespace.renameSync(logFile, logFile + ".prev");
1980
+ } catch {
1981
+ }
1982
+ logStream = fs__namespace.createWriteStream(logFile, { flags: "a" });
1983
+ const origLog = console.log;
1984
+ const origErr = console.error;
1985
+ console.log = (...args) => {
1986
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${args.map(String).join(" ")}`;
1987
+ logStream?.write(line + "\n");
1988
+ origLog(...args);
1989
+ };
1990
+ console.error = (...args) => {
1991
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ERROR ${args.map(String).join(" ")}`;
1992
+ logStream?.write(line + "\n");
1993
+ origErr(...args);
1994
+ };
1995
+ console.log(`[Maestro] Log file: ${logFile}`);
1996
+ } catch (err) {
1997
+ console.error("[Maestro] Failed to set up log file:", err);
1998
+ }
1871
1999
  const startupArgs = process.argv.slice(2);
1872
2000
  let startupPlanFile = null;
1873
2001
  const pfIdx = startupArgs.indexOf("--plan-file");
@@ -129,6 +129,15 @@ wss.on('connection', (ws) => {
129
129
  ws.send(JSON.stringify({ type: 'pong' }));
130
130
  break;
131
131
  case 'create': {
132
+ // If session already exists (reconnect after connection drop), reattach to new WebSocket
133
+ if (sessions.has(msg.id)) {
134
+ const proc = sessions.get(msg.id);
135
+ proc.removeAllListeners('data');
136
+ proc.onData((data) => ws.send(JSON.stringify({ type: 'data', id: msg.id, data: Buffer.from(data).toString('base64') })));
137
+ ws.send(JSON.stringify({ type: 'created', id: msg.id }));
138
+ console.log('[agent] session reattached:', msg.id);
139
+ break;
140
+ }
132
141
  const proc = pty.spawn('powershell.exe', [], {
133
142
  name: 'xterm-256color', cols: msg.cols, rows: msg.rows, cwd: msg.cwd,
134
143
  env: { ...process.env },
@@ -167,7 +176,7 @@ wss.on('connection', (ws) => {
167
176
  }
168
177
  }
169
178
  });
170
- ws.on('close', () => { console.log('[agent] client disconnected'); for (const [, proc] of sessions) proc.kill(); sessions.clear(); });
179
+ ws.on('close', () => { console.log('[agent] client disconnected sessions kept alive for reconnect'); });
171
180
  });
172
181
 
173
182
  console.log(\`[agent] Maestro agent listening on ws://127.0.0.1:\${PORT}\`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rajat-rastogi/maestro",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "Conduct your codebase — autonomous AI coding orchestrator",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",