From e4e1d3fb92a2e1ebb8e9c252d37e8e2750023e6f Mon Sep 17 00:00:00 2001 From: xingyu <1294266616@qq.com> Date: Sat, 7 Feb 2026 16:17:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Windows=20platform=20improvements=20?= =?UTF-8?q?=E2=80=94=20re-enable=20Chroma,=20fix=20DB=20race,=20simplify?= =?UTF-8?q?=20env=20isolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/services/infrastructure/ProcessManager.ts | 28 ++++---- src/services/sync/ChromaSync.ts | 28 ++------ src/shared/EnvManager.ts | 71 ++++++++----------- 3 files changed, 49 insertions(+), 78 deletions(-) diff --git a/src/services/infrastructure/ProcessManager.ts b/src/services/infrastructure/ProcessManager.ts index e536f7c9..9d128afd 100644 --- a/src/services/infrastructure/ProcessManager.ts +++ b/src/services/infrastructure/ProcessManager.ts @@ -101,7 +101,7 @@ export async function getChildProcesses(parentPid: number): Promise { 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 { 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 { * 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; diff --git a/src/services/sync/ChromaSync.ts b/src/services/sync/ChromaSync.ts index 98d2fa5b..95bb1018 100644 --- a/src/services/sync/ChromaSync.ts +++ b/src/services/sync/ChromaSync.ts @@ -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); diff --git a/src/shared/EnvManager.ts b/src/shared/EnvManager.ts index 8c1518d7..6495b917 100644 --- a/src/shared/EnvManager.ts +++ b/src/shared/EnvManager.ts @@ -18,33 +18,15 @@ import { logger } from '../utils/logger.js'; const DATA_DIR = join(homedir(), '.claude-mem'); export const ENV_FILE_PATH = join(DATA_DIR, '.env'); -// Essential system environment variables that subprocesses need to function -const ESSENTIAL_SYSTEM_VARS = [ - 'PATH', - 'HOME', - 'USER', - 'SHELL', - 'TMPDIR', - 'TMP', - 'TEMP', - 'LANG', - 'LC_ALL', - 'LC_CTYPE', - // Node.js specific - 'NODE_ENV', - 'NODE_PATH', - // Platform specific - 'SYSTEMROOT', // Windows - 'WINDIR', // Windows - 'PROGRAMFILES', // Windows - 'APPDATA', // Windows - 'LOCALAPPDATA', // Windows - 'XDG_RUNTIME_DIR', // Linux - 'XDG_CONFIG_HOME', // Linux - 'XDG_DATA_HOME', // Linux - // Claude Code specific (not credentials) - 'CLAUDE_CONFIG_DIR', - 'CLAUDE_CODE_DEBUG_LOGS_DIR', +// Environment variables to STRIP from subprocess environment (blocklist approach) +// Only ANTHROPIC_API_KEY is stripped because it's the specific variable that causes +// Issue #733: project .env files set ANTHROPIC_API_KEY which the SDK auto-discovers, +// causing memory operations to bill personal API accounts instead of CLI subscription. +// +// All other env vars (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, system vars, etc.) +// are passed through to avoid breaking CLI authentication, proxies, and platform features. +const BLOCKED_ENV_VARS = [ + 'ANTHROPIC_API_KEY', // Issue #733: Prevent auto-discovery from project .env files ]; // Credential keys that claude-mem manages @@ -191,39 +173,44 @@ export function saveClaudeMemEnv(env: ClaudeMemEnv): void { } /** - * Build a clean, isolated environment for spawning SDK subprocesses + * Build a clean environment for spawning SDK subprocesses * - * This is the key function that prevents Issue #733: - * - Includes only essential system variables (PATH, HOME, etc.) - * - Adds credentials ONLY from claude-mem's .env file - * - Does NOT inherit random ANTHROPIC_API_KEY from user's shell + * Uses a BLOCKLIST approach: inherits the full process environment but strips + * only ANTHROPIC_API_KEY to prevent Issue #733 (accidental billing from project .env files). * - * @param includeCredentials - Whether to include API keys (default: true) + * All other variables pass through, including: + * - ANTHROPIC_AUTH_TOKEN (CLI subscription auth) + * - ANTHROPIC_BASE_URL (custom proxy endpoints) + * - Platform-specific vars (USERPROFILE, XDG_*, etc.) + * + * If claude-mem has an explicit ANTHROPIC_API_KEY in ~/.claude-mem/.env, it's re-injected + * after stripping, so the managed credential takes precedence over any ambient value. + * + * @param includeCredentials - Whether to include API keys from ~/.claude-mem/.env (default: true) */ export function buildIsolatedEnv(includeCredentials: boolean = true): Record { + // 1. Start with full process environment const isolatedEnv: Record = {}; - - // 1. Copy essential system variables from current process - for (const key of ESSENTIAL_SYSTEM_VARS) { - const value = process.env[key]; - if (value !== undefined) { + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined && !BLOCKED_ENV_VARS.includes(key)) { isolatedEnv[key] = value; } } - // 2. Add SDK entrypoint marker + // 2. Override SDK entrypoint marker isolatedEnv.CLAUDE_CODE_ENTRYPOINT = 'sdk-ts'; - // 3. Add credentials from claude-mem's .env file (NOT from process.env) + // 3. Re-inject managed credentials from claude-mem's .env file if (includeCredentials) { const credentials = loadClaudeMemEnv(); // Only add ANTHROPIC_API_KEY if explicitly configured in claude-mem - // If not configured, CLI billing will be used (via pathToClaudeCodeExecutable) + // If not configured, CLI billing will be used (via ANTHROPIC_AUTH_TOKEN passthrough) if (credentials.ANTHROPIC_API_KEY) { isolatedEnv.ANTHROPIC_API_KEY = credentials.ANTHROPIC_API_KEY; } - // Note: GEMINI_API_KEY and OPENROUTER_API_KEY are handled by their respective agents + // Note: GEMINI_API_KEY and OPENROUTER_API_KEY pass through from process.env, + // but claude-mem's .env takes precedence if configured if (credentials.GEMINI_API_KEY) { isolatedEnv.GEMINI_API_KEY = credentials.GEMINI_API_KEY; }