fix: Windows platform improvements — re-enable Chroma, fix DB race, simplify env isolation
1. ProcessManager: Migrate spawnDaemon() from WMIC to PowerShell Start-Process - WMIC deprecated in Windows 11, PowerShell inherits env vars properly - Use -WindowStyle Hidden to prevent console popups - Fix redundant backslash escaping in PowerShell $_ variables 2. ChromaSync: Re-enable vector search on Windows - Remove overly defensive platform check that disabled all semantic search - Worker daemon starts with -WindowStyle Hidden; child processes inherit - MCP SDK's StdioClientTransport uses shell:false, no new console created 3. worker-service: Unified DB-ready gate middleware - Replace single-endpoint /api/sessions/init wait with global middleware - Hold all DB-dependent requests until database is initialized (30s timeout) - Whitelist static assets, /health, and viewer page for immediate response - Separate dbReadyPromise (DB only) from initializationComplete (full init) - Fixes "Database not initialized" errors on /stream, /summarize, /init 4. EnvManager: Switch from allowlist to blocklist for subprocess env - Only strip ANTHROPIC_API_KEY to prevent Issue #733 billing hijack - Pass through all other vars (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, etc.) - Simpler, less fragile than maintaining an exhaustive system vars allowlist
This commit is contained in:
@@ -101,7 +101,7 @@ export async function getChildProcesses(parentPid: number): Promise<number[]> {
|
||||
|
||||
try {
|
||||
// PowerShell Get-Process instead of WMIC (deprecated in Windows 11)
|
||||
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { \\$_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty Id"`;
|
||||
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty Id"`;
|
||||
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
|
||||
// PowerShell outputs just numbers (one per line), simpler than WMIC's "ProcessId=1234" format
|
||||
return stdout
|
||||
@@ -226,10 +226,10 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
|
||||
if (isWindows) {
|
||||
// Windows: Use PowerShell Get-CimInstance with JSON output for age filtering
|
||||
const patternConditions = ORPHAN_PROCESS_PATTERNS
|
||||
.map(p => `\\$_.CommandLine -like '*${p}*'`)
|
||||
.map(p => `$_.CommandLine -like '*${p}*'`)
|
||||
.join(' -or ');
|
||||
|
||||
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { (${patternConditions}) -and \\$_.ProcessId -ne ${currentPid} } | Select-Object ProcessId, CreationDate | ConvertTo-Json"`;
|
||||
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { (${patternConditions}) -and $_.ProcessId -ne ${currentPid} } | Select-Object ProcessId, CreationDate | ConvertTo-Json"`;
|
||||
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
|
||||
|
||||
if (!stdout.trim() || stdout.trim() === 'null') {
|
||||
@@ -339,9 +339,9 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
|
||||
* Spawn a detached daemon process
|
||||
* Returns the child PID or undefined if spawn failed
|
||||
*
|
||||
* On Windows, uses WMIC to spawn a truly independent process that
|
||||
* survives parent exit without console popups. WMIC creates processes
|
||||
* that are not associated with the parent's console.
|
||||
* On Windows, uses PowerShell Start-Process with -WindowStyle Hidden to spawn
|
||||
* a truly independent process without console popups. Unlike WMIC, PowerShell
|
||||
* inherits environment variables from the parent process.
|
||||
*
|
||||
* On Unix, uses standard detached spawn.
|
||||
*
|
||||
@@ -361,21 +361,19 @@ export function spawnDaemon(
|
||||
};
|
||||
|
||||
if (isWindows) {
|
||||
// Use WMIC to spawn a process that's independent of the parent console
|
||||
// This avoids the console popup that occurs with detached: true
|
||||
// Paths must be individually quoted for WMIC when they contain spaces
|
||||
// Use PowerShell Start-Process to spawn a hidden, independent process
|
||||
// Unlike WMIC, PowerShell inherits environment variables from parent
|
||||
// -WindowStyle Hidden prevents console popup
|
||||
const execPath = process.execPath;
|
||||
const script = scriptPath;
|
||||
// WMIC command format: wmic process call create "\"path1\" \"path2\" args"
|
||||
const command = `wmic process call create "\\"${execPath}\\" \\"${script}\\" --daemon"`;
|
||||
const psCommand = `Start-Process -FilePath '${execPath}' -ArgumentList '${script}','--daemon' -WindowStyle Hidden`;
|
||||
|
||||
try {
|
||||
execSync(command, {
|
||||
execSync(`powershell -NoProfile -Command "${psCommand}"`, {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
windowsHide: true,
|
||||
env
|
||||
});
|
||||
// WMIC returns immediately, we can't get the spawned PID easily
|
||||
// Worker will write its own PID file after listen()
|
||||
return 0;
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
@@ -85,25 +85,15 @@ export class ChromaSync {
|
||||
private readonly VECTOR_DB_DIR: string;
|
||||
private readonly BATCH_SIZE = 100;
|
||||
|
||||
// Windows: Chroma disabled due to MCP SDK spawning console popups
|
||||
// See: https://github.com/anthropics/claude-mem/issues/675
|
||||
// Will be re-enabled when we migrate to persistent HTTP server
|
||||
private readonly disabled: boolean;
|
||||
// Windows popup concern resolved: the worker daemon starts with -WindowStyle Hidden,
|
||||
// so child processes (uvx/chroma-mcp) inherit the hidden console and don't create new windows.
|
||||
// MCP SDK's StdioClientTransport uses shell:false and no detached flag, so console is inherited.
|
||||
private readonly disabled: boolean = false;
|
||||
|
||||
constructor(project: string) {
|
||||
this.project = project;
|
||||
this.collectionName = `cm__${project}`;
|
||||
this.VECTOR_DB_DIR = path.join(os.homedir(), '.claude-mem', 'vector-db');
|
||||
|
||||
// Disable on Windows to prevent console popups from MCP subprocess spawning
|
||||
// The MCP SDK's StdioClientTransport spawns Python processes that create visible windows
|
||||
this.disabled = process.platform === 'win32';
|
||||
if (this.disabled) {
|
||||
logger.warn('CHROMA_SYNC', 'Vector search disabled on Windows (prevents console popups)', {
|
||||
project: this.project,
|
||||
reason: 'MCP SDK subprocess spawning causes visible console windows'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +193,6 @@ export class ChromaSync {
|
||||
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Get combined SSL certificate bundle for Zscaler/corporate proxy environments
|
||||
const combinedCertPath = this.getCombinedCertPath();
|
||||
@@ -232,12 +221,9 @@ export class ChromaSync {
|
||||
});
|
||||
}
|
||||
|
||||
// CRITICAL: On Windows, try to hide console window to prevent PowerShell popups
|
||||
// Note: windowsHide may not be supported by MCP SDK's StdioClientTransport
|
||||
if (isWindows) {
|
||||
transportOptions.windowsHide = true;
|
||||
logger.debug('CHROMA_SYNC', 'Windows detected, attempting to hide console window', { project: this.project });
|
||||
}
|
||||
// Note: windowsHide is not needed here because the worker daemon starts with
|
||||
// -WindowStyle Hidden, so child processes inherit the hidden console.
|
||||
// The MCP SDK ignores custom windowsHide anyway (overridden internally).
|
||||
|
||||
this.transport = new StdioClientTransport(transportOptions);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user