Files
ccs/src/cliproxy/executor/session-bridge.ts
T
Kai (Tam Nhu) TranandGitHub 5d27f10367 refactor(cliproxy): remove orphaned bg keepalive wiring (#1353)
Red-team review of #1340 found that backgroundProbe / shouldKeepSessionProxiesAlive /
startKeepAliveWatcher and related types were never reachable: the sole caller
(claude-launcher.ts) passed no hasBackgroundWorkerUsingBaseUrl detector, so
backgroundProbe was always undefined and the keepalive branch never executed.

Remove the unreachable scaffold:
- Delete CLAUDE_BG_WORKER_ARGS, hasClaudeBackgroundWorkerUsingBaseUrl{,InProcessList},
  shouldKeepSessionProxiesAlive, ShouldKeepSessionProxiesAliveOptions, and the
  SessionProxyKeepAliveOptions interface from session-bridge.ts
- Remove the backgroundProbe/startKeepAliveWatcher block and keepalive exit branch
  from setupCleanupHandlers; the function now always calls stopSessionResources on
  Claude exit
- Remove backgroundKeepAliveBaseUrl wiring from claude-launcher.ts
- Remove the execFileSync import (was only used by the deleted ps-based detector)
- Update test file: remove tests for deleted exports, keep placeholder

A trusted bg-worker detector can be re-added in a future PR if the feature is
actually needed; the keepalive logic in shouldKeepSessionProxiesAlive can be
restored then.
2026-05-23 17:29:18 -04:00

239 lines
7.5 KiB
TypeScript

/**
* Session Bridge - Integration with session tracking and proxy detection
*
* Handles:
* - Session registration and unregistration
* - Proxy detection and version checking
* - Orphaned proxy reclamation
* - Startup lock coordination
*/
import { ChildProcess } from 'child_process';
import { info, warn } from '../../utils/ui';
import { getInstalledCliproxyVersion } from '../binary-manager';
import { CLIProxyBackend } from '../types';
import {
cleanupOrphanedSessions,
registerSession,
unregisterSession,
stopProxy,
} from '../session-tracker';
import {
detectRunningProxy,
waitForProxyHealthy,
reclaimOrphanedProxy,
} from '../proxy/proxy-detector';
import { withStartupLock } from '../services/startup-lock';
import { killProcessOnPort } from '../../utils/platform-commands';
import { stopQuotaMonitor } from '../quota/quota-manager';
export interface ProxySessionResult {
sessionId?: string;
proxy?: ChildProcess;
shouldSpawn: boolean;
}
/**
* Check for existing proxy and handle version mismatch, or determine if new spawn needed
*/
export async function checkOrJoinProxy(
port: number,
timeout: number,
verbose: boolean
): Promise<ProxySessionResult> {
const log = (msg: string) => {
if (verbose) {
console.error(`[cliproxy] ${msg}`);
}
};
// Cleanup orphaned sessions before detection
cleanupOrphanedSessions(port);
let sessionId: string | undefined;
let shouldSpawn = false;
// Use startup lock to coordinate with other CCS processes
await withStartupLock(async () => {
// Detect running proxy using multiple methods (HTTP, session-lock, port-process)
let proxyStatus = await detectRunningProxy(port);
log(`Proxy detection: ${JSON.stringify(proxyStatus)}`);
// Check for version mismatch - restart proxy if installed version differs from running
if (proxyStatus.running && proxyStatus.verified && proxyStatus.version) {
const installedVersion = getInstalledCliproxyVersion();
if (installedVersion !== proxyStatus.version) {
console.log(
warn(
`Version mismatch: running v${proxyStatus.version}, installed v${installedVersion}. Restarting proxy...`
)
);
log(`Stopping outdated proxy (PID: ${proxyStatus.pid ?? 'unknown'})...`);
const stopResult = await stopProxy(port);
if (stopResult.stopped) {
log(`Stopped outdated proxy successfully`);
} else {
log(`Stop proxy result: ${stopResult.error ?? 'unknown error'}`);
}
// Wait for port to be released
await new Promise((r) => setTimeout(r, 500));
// Re-detect proxy status (should now be not running)
proxyStatus = await detectRunningProxy(port);
log(`Re-detection after version mismatch restart: ${JSON.stringify(proxyStatus)}`);
}
}
if (proxyStatus.running && proxyStatus.verified) {
// Healthy proxy found - join it
if (proxyStatus.pid) {
sessionId = reclaimOrphanedProxy(port, proxyStatus.pid, verbose) ?? undefined;
}
if (sessionId) {
console.log(info(`Joined existing CLIProxy on port ${port} (${proxyStatus.method})`));
} else {
// Failed to register session - proxy is running but we can't track it
console.log(info(`Using existing CLIProxy on port ${port} (session tracking unavailable)`));
log(`PID=${proxyStatus.pid ?? 'unknown'}, session registration skipped`);
}
return; // Exit lock early, skip spawning
}
if (proxyStatus.running && !proxyStatus.verified) {
// Proxy detected but not ready yet (another process is starting it)
log(`Proxy starting up (detected via ${proxyStatus.method}), waiting...`);
const becameHealthy = await waitForProxyHealthy(port, timeout);
if (becameHealthy) {
if (proxyStatus.pid) {
sessionId = reclaimOrphanedProxy(port, proxyStatus.pid, verbose) ?? undefined;
}
console.log(info(`Joined CLIProxy after startup wait`));
return; // Exit lock early
}
// Proxy didn't become healthy - kill and respawn
if (proxyStatus.pid) {
log(`Proxy PID ${proxyStatus.pid} not responding, killing...`);
killProcessOnPort(port, verbose);
await new Promise((r) => setTimeout(r, 500));
}
}
if (proxyStatus.blocked && proxyStatus.blocker) {
// Port blocked by non-CLIProxy process
// Last resort: try HTTP health check (handles Windows PID-XXXXX case)
const isActuallyOurs = await waitForProxyHealthy(port, 1000);
if (isActuallyOurs) {
sessionId = reclaimOrphanedProxy(port, proxyStatus.blocker.pid, verbose) ?? undefined;
console.log(info(`Reclaimed CLIProxy with unrecognized process name`));
return;
}
// Truly blocked by another application
const { getPortCheckCommand } = await import('../../utils/platform-commands');
console.error('');
console.error(
warn(
`Port ${port} is blocked by ${proxyStatus.blocker.processName} (PID ${proxyStatus.blocker.pid})`
)
);
console.error('');
console.error('To fix this, close the blocking application or run:');
console.error(` ${getPortCheckCommand(port)}`);
console.error('');
throw new Error(`Port ${port} is in use by another application`);
}
// No proxy found - need to spawn
shouldSpawn = true;
});
return { sessionId, shouldSpawn };
}
/**
* Register a new proxy session after spawning
*/
export function registerProxySession(
port: number,
pid: number,
backend: CLIProxyBackend,
verbose: boolean
): string {
const installedVersion = getInstalledCliproxyVersion();
const sessionId = registerSession(port, pid, installedVersion, backend);
if (verbose) {
console.error(
`[cliproxy] Registered session ${sessionId} with new proxy (PID ${pid}, version ${installedVersion})`
);
}
return sessionId;
}
/**
* Setup cleanup handlers for session unregistration
*/
export function setupCleanupHandlers(
claude: ChildProcess,
sessionId: string | undefined,
sessionPort: number,
codexReasoningProxy: unknown,
toolSanitizationProxy: unknown,
httpsTunnel: unknown,
verbose: boolean
): void {
const log = (msg: string) => {
if (verbose) {
console.error(`[cliproxy] ${msg}`);
}
};
let resourcesStopped = false;
const stopResource = (resource: unknown) => {
if (resource && typeof resource === 'object' && 'stop' in resource) {
(resource as { stop: () => void }).stop();
}
};
const stopSessionResources = () => {
if (resourcesStopped) return;
resourcesStopped = true;
stopQuotaMonitor();
stopResource(codexReasoningProxy);
stopResource(toolSanitizationProxy);
stopResource(httpsTunnel);
if (sessionId) {
unregisterSession(sessionId, sessionPort);
log(`Session ${sessionId} unregistered, proxy persists for other sessions or future use`);
}
};
const cleanup = () => {
log('Parent signal received, cleaning up');
stopSessionResources();
claude.kill('SIGTERM');
};
claude.on('exit', (code, signal) => {
log(`Claude exited: code=${code}, signal=${signal}`);
stopSessionResources();
if (signal) {
process.kill(process.pid, signal as NodeJS.Signals);
} else {
process.exit(code || 0);
}
});
claude.on('error', (error) => {
console.error(require('../../utils/ui').fail(`Claude CLI error: ${error}`));
stopSessionResources();
process.exit(1);
});
process.once('SIGTERM', cleanup);
process.once('SIGINT', cleanup);
}