@rikcodes/teamclaude 1.1.20-rik.6 → 1.1.20-rik.7

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/package.json +1 -1
  2. package/src/tui.js +22 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rikcodes/teamclaude",
3
- "version": "1.1.20-rik.6",
3
+ "version": "1.1.20-rik.7",
4
4
  "description": "Multi-account proxy for Claude Code and Codex: pools Claude Max, ChatGPT/Codex, API-key and third-party backend accounts, and rotates on quota",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/tui.js CHANGED
@@ -513,6 +513,15 @@ export class TUI {
513
513
  // thing blocked. Non-blocking here, and the paint below drops a frame
514
514
  // when the terminal is behind instead of waiting for it.
515
515
  this._setStdoutBlocking(false);
516
+ // The other half of that bargain: a non-blocking write reports failure
517
+ // asynchronously, as an 'error' event on the stream, and an unhandled one
518
+ // becomes an uncaughtException that ends the process. So a terminal going
519
+ // away — a pane closed, a pty recreated — took the whole proxy with it:
520
+ // two EPIPE crashes, 2026-09-15 and 2026-09-16, both from this path, each
521
+ // leaving the sidecar orphaned on its port. A display may no more kill the
522
+ // proxy than block it. Record that stdout is gone and serve on without it.
523
+ this._stdoutErrorHandler = () => { this._stdoutBroken = true; };
524
+ process.stdout.on('error', this._stdoutErrorHandler);
516
525
  process.stdout.write(`${ESC}?1049h${ESC}?25l`);
517
526
  process.stdin.setRawMode(true);
518
527
  process.stdin.resume();
@@ -575,7 +584,15 @@ export class TUI {
575
584
  // screen with no cursor is the one state an operator cannot recover
576
585
  // without knowing the escape by heart.
577
586
  this._setStdoutBlocking(true);
578
- process.stdout.write(`${ESC}?25h${ESC}?1049l`);
587
+ // Blocking again means a failed write throws here instead of arriving as
588
+ // an event, and a terminal that has already gone will fail. Restoring the
589
+ // screen is best-effort: there is nobody left to restore it for. The error
590
+ // listener outlives this write, so a late async failure is absorbed too.
591
+ try { process.stdout.write(`${ESC}?25h${ESC}?1049l`); } catch { /* terminal already gone */ }
592
+ if (this._stdoutErrorHandler) {
593
+ process.stdout.removeListener('error', this._stdoutErrorHandler);
594
+ this._stdoutErrorHandler = null;
595
+ }
579
596
  try { process.stdin.setRawMode(false); } catch {}
580
597
  process.stdin.pause();
581
598
  }
@@ -1384,6 +1401,10 @@ export class TUI {
1384
1401
  * again costs a wake-up and a terminal round trip to change nothing.
1385
1402
  */
1386
1403
  _paint(buf, force) {
1404
+ // stdout has already failed once: the terminal is gone, every further
1405
+ // write would fail the same way, and a stream that never drains would
1406
+ // strand the pending-paint handshake below. Serving continues blind.
1407
+ if (this._stdoutBroken) return;
1387
1408
  const stale = Date.now() - (this._lastPaintAt || 0) >= FORCE_REPAINT_MS;
1388
1409
  if (!force && !stale && buf === this._lastFrame) return;
1389
1410
  // The terminal has not taken the previous frame yet. Painting anyway would