(cfg: ServerConfig)
| 1467 | * responsibility — embedders may fd-pass; CLI uses Bun.serve normally. |
| 1468 | */ |
| 1469 | export function buildFetchHandler(cfg: ServerConfig): ServerHandle { |
| 1470 | if (!cfg.authToken || cfg.authToken.length < 16) { |
| 1471 | throw new Error('buildFetchHandler: cfg.authToken must be a non-empty string >= 16 chars'); |
| 1472 | } |
| 1473 | if (!cfg.browserManager) { |
| 1474 | throw new Error('buildFetchHandler: cfg.browserManager is required'); |
| 1475 | } |
| 1476 | |
| 1477 | // Re-run init with cfg-provided values. ensureStateDir is idempotent |
| 1478 | // (mkdir -p); initAuditLog is idempotent (sets a module string); |
| 1479 | // initRegistry is idempotent for same-token, throws for different-token. |
| 1480 | // Owning init here (instead of at module load) means cfg.authToken is the |
| 1481 | // single source of truth for the registry root token. |
| 1482 | ensureStateDir(cfg.config); |
| 1483 | initAuditLog(cfg.config.auditLog); |
| 1484 | initRegistry(cfg.authToken); |
| 1485 | |
| 1486 | const { authToken, browserManager: cfgBrowserManager, startTime, beforeRoute, browsePort } = cfg; |
| 1487 | // Strict opt-out: only explicit `false` flips the gate. Any other value |
| 1488 | // (undefined, truthy non-bool from a JS caller bypassing TS, etc.) defaults |
| 1489 | // to gstack-owns. Matches the "default-true preserves CLI bit-for-bit" |
| 1490 | // premise even under malformed cfg. |
| 1491 | const ownsTerminalAgent = cfg.ownsTerminalAgent === false ? false : true; |
| 1492 | |
| 1493 | // ─── Terminal-Agent Watchdog (v1.44+) ───────────────────────────── |
| 1494 | // |
| 1495 | // The terminal-agent process can die independently of the server: SIGKILL |
| 1496 | // from the OS OOM killer, an uncaught exception under load, an external |
| 1497 | // `pkill` from a sibling debugging session. Pre-v1.44 the sidebar would |
| 1498 | // see the broken connection and stay broken until the user reloaded. |
| 1499 | // Now: 60s ticker checks the recorded agent PID, respawns via the shared |
| 1500 | // spawnTerminalAgent helper if dead. |
| 1501 | // |
| 1502 | // Identity-based — uses readAgentRecord + isProcessAlive, NOT a process |
| 1503 | // name probe. Critical: prevents respawning around a slow-but-alive agent |
| 1504 | // (which would create split-brain — two agents writing the port file, |
| 1505 | // tokens diverging between them, mystery PTY upgrade failures). |
| 1506 | // |
| 1507 | // Crash-loop guard: 3 respawn attempts inside 60s → stop trying and emit |
| 1508 | // a one-line error. Manual `forceRestart` from the sidebar clears the |
| 1509 | // history (the user is the explicit signal to retry). |
| 1510 | // |
| 1511 | // Only active when ownsTerminalAgent === true. Embedders that pre-launch |
| 1512 | // their own PTY server (gbrowser phoenix overlay) must not be auto-respawned |
| 1513 | // by us — their lifecycle is their concern. |
| 1514 | let agentWatchdogInterval: ReturnType<typeof setInterval> | null = null; |
| 1515 | const respawnHistory: number[] = []; |
| 1516 | const AGENT_WATCHDOG_TICK_MS = parseInt( |
| 1517 | process.env.GSTACK_AGENT_WATCHDOG_TICK_MS || '60000', |
| 1518 | 10, |
| 1519 | ); |
| 1520 | const RESPAWN_GUARD_WINDOW_MS = 60_000; |
| 1521 | const RESPAWN_GUARD_MAX = 3; |
| 1522 | let agentRespawnGuardTripped = false; |
| 1523 | |
| 1524 | if (ownsTerminalAgent) { |
| 1525 | agentWatchdogInterval = setInterval(() => { |
| 1526 | if (isShuttingDown) return; |
no test coverage detected