fix: keep cookie picker alive after cli exits

Fixes garrytan/gstack#985
This commit is contained in:
d 🔹
2026-04-14 07:53:35 +00:00
committed by Garry Tan
parent ef17a5c807
commit a4cbcadeaf
3 changed files with 86 additions and 9 deletions
+17
View File
@@ -40,6 +40,23 @@ export function generatePickerCode(): string {
return code;
}
/** Return true while the picker still has a live code or session. */
export function hasActivePicker(): boolean {
const now = Date.now();
for (const [code, expiry] of pendingCodes) {
if (expiry > now) return true;
pendingCodes.delete(code);
}
for (const [session, expiry] of validSessions) {
if (expiry > now) return true;
validSessions.delete(session);
}
return false;
}
/** Extract session ID from the gstack_picker cookie. */
function getSessionFromCookie(req: Request): string | null {
const cookie = req.headers.get('cookie');
+17 -8
View File
@@ -764,14 +764,18 @@ if (BROWSE_PARENT_PID > 0) {
try {
process.kill(BROWSE_PARENT_PID, 0); // signal 0 = existence check only, no signal sent
} catch {
// Parent exited. Behavior depends on mode:
// - Normal (headless) mode: stay alive. Claude Code's Bash tool kills the
// parent shell between invocations, so the server must survive. The
// idle timeout (30 min) handles eventual cleanup.
// - Headed / tunnel mode: the idle timeout DOESN'T apply (see idleCheckInterval
// above — both modes early-return). If we ignored parent death here too,
// orphan daemons would accumulate forever after /pair-agent or
// /open-gstack-browser sessions end. Shutdown instead.
// Parent exited. Resolution order:
// 1. Active cookie picker (one-time code or session live)? Stay alive
// regardless of mode — tearing down the server mid-import leaves the
// picker UI with a stale "Failed to fetch" error.
// 2. Headed / tunnel mode? Shutdown. The idle timeout doesn't apply in
// these modes (see idleCheckInterval above — both early-return), so
// ignoring parent death here would leak orphan daemons after
// /pair-agent or /open-gstack-browser sessions.
// 3. Normal (headless) mode? Stay alive. Claude Code's Bash tool kills
// the parent shell between invocations. The idle timeout (30 min)
// handles eventual cleanup.
if (hasActivePicker()) return;
const headed = browserManager.getConnectionMode() === 'headed';
if (headed || tunnelActive) {
console.log(`[browse] Parent process ${BROWSE_PARENT_PID} exited in ${headed ? 'headed' : 'tunnel'} mode, shutting down`);
@@ -785,6 +789,7 @@ if (BROWSE_PARENT_PID > 0) {
}
// ─── Command Sets (from commands.ts — single source of truth) ───
import { hasActivePicker } from './cookie-picker-routes';
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
export { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS };
@@ -1250,6 +1255,10 @@ process.on('SIGINT', shutdown);
// SIGTERM so external tooling (systemd, supervisord, CI) can shut cleanly
// without waiting forever. Ctrl+C and /stop still work either way.
process.on('SIGTERM', () => {
if (hasActivePicker()) {
console.log('[browse] Received SIGTERM but cookie picker is active, ignoring to avoid stranding the picker UI');
return;
}
const headed = browserManager.getConnectionMode() === 'headed';
if (headed || tunnelActive) {
console.log(`[browse] Received SIGTERM in ${headed ? 'headed' : 'tunnel'} mode, shutting down`);