fix: eliminate Windows console popups during daemon spawn and Chroma operations (#751)

* fix: eliminate Windows console popups during daemon spawn and Chroma operations

Two changes to fix Windows Terminal popup issues:

1. Worker daemon spawn (ProcessManager.spawnDaemon):
   - Windows: Use WMIC to spawn truly independent process without console
   - WMIC creates processes that survive parent exit and have no console association
   - Properly handles paths with spaces via double-quoting
   - Unix: Unchanged behavior with standard detached spawn

2. PID file handling (worker-service.ts):
   - Worker now writes its own PID after listen() succeeds (all platforms)
   - Removes race condition where spawner wrote PID before worker was ready
   - On Windows, spawner PID was useless anyway

3. Chroma vector search (ChromaSync.ts):
   - Temporarily disabled on Windows to prevent MCP SDK subprocess popups
   - Will be re-enabled when we migrate to persistent HTTP server architecture
   - Windows users still get full observation storage, just no semantic search

Tested on Windows 11 via SSH - worker spawns without console popups,
survives parent process exit, and all lifecycle commands (start/stop/restart)
work correctly.

Fixes #748, #708, #681, #676, #675

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add YAML frontmatter to slash commands for discoverability

Commands /do and /make-plan were not appearing in Claude Code because
they lacked the required YAML frontmatter metadata. Added description
and argument-hint fields to both commands.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: bigphoot <bigphoot@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alexander Knigge
2026-01-22 15:46:23 -08:00
committed by GitHub
parent 901cff909e
commit e6ae017609
6 changed files with 311 additions and 222 deletions
+5
View File
@@ -1,3 +1,8 @@
---
description: "Execute a plan using subagents for implementation"
argument-hint: "[task or plan reference]"
---
You are an ORCHESTRATOR.
Primary instruction: deploy subagents to execute *all* work for #$ARGUMENTS.
+5
View File
@@ -1,3 +1,8 @@
---
description: "Create an implementation plan with documentation discovery"
argument-hint: "[feature or task description]"
---
You are an ORCHESTRATOR.
Create an LLM-friendly plan in phases that can be executed consecutively in new chat contexts.
File diff suppressed because one or more lines are too long
+41 -6
View File
@@ -262,21 +262,55 @@ 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 Unix, uses standard detached spawn.
*
* PID file is written by the worker itself after listen() succeeds,
* not by the spawner (race-free, works on all platforms).
*/
export function spawnDaemon(
scriptPath: string,
port: number,
extraEnv: Record<string, string> = {}
): number | undefined {
const isWindows = process.platform === 'win32';
const env = {
...process.env,
CLAUDE_MEM_WORKER_PORT: String(port),
...extraEnv
};
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
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"`;
try {
execSync(command, {
stdio: 'ignore',
windowsHide: true
});
// 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;
}
}
// Unix: standard detached spawn
const child = spawn(process.execPath, [scriptPath, '--daemon'], {
detached: true,
stdio: 'ignore',
windowsHide: true,
env: {
...process.env,
CLAUDE_MEM_WORKER_PORT: String(port),
...extraEnv
}
env
});
if (child.pid === undefined) {
@@ -284,6 +318,7 @@ export function spawnDaemon(
}
child.unref();
return child.pid;
}
+39
View File
@@ -83,10 +83,32 @@ 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;
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'
});
}
}
/**
* Check if Chroma is disabled (Windows)
*/
isDisabled(): boolean {
return this.disabled;
}
/**
@@ -387,6 +409,7 @@ export class ChromaSync {
/**
* Sync a single observation to Chroma
* Blocks until sync completes, throws on error
* No-op on Windows (Chroma disabled to prevent console popups)
*/
async syncObservation(
observationId: number,
@@ -397,6 +420,8 @@ export class ChromaSync {
createdAtEpoch: number,
discoveryTokens: number = 0
): Promise<void> {
if (this.disabled) return;
// Convert ParsedObservation to StoredObservation format
const stored: StoredObservation = {
id: observationId,
@@ -431,6 +456,7 @@ export class ChromaSync {
/**
* Sync a single summary to Chroma
* Blocks until sync completes, throws on error
* No-op on Windows (Chroma disabled to prevent console popups)
*/
async syncSummary(
summaryId: number,
@@ -441,6 +467,8 @@ export class ChromaSync {
createdAtEpoch: number,
discoveryTokens: number = 0
): Promise<void> {
if (this.disabled) return;
// Convert ParsedSummary to StoredSummary format
const stored: StoredSummary = {
id: summaryId,
@@ -491,6 +519,7 @@ export class ChromaSync {
/**
* Sync a single user prompt to Chroma
* Blocks until sync completes, throws on error
* No-op on Windows (Chroma disabled to prevent console popups)
*/
async syncUserPrompt(
promptId: number,
@@ -500,6 +529,8 @@ export class ChromaSync {
promptNumber: number,
createdAtEpoch: number
): Promise<void> {
if (this.disabled) return;
// Create StoredUserPrompt format
const stored: StoredUserPrompt = {
id: promptId,
@@ -614,8 +645,11 @@ export class ChromaSync {
* Backfill: Sync all observations missing from Chroma
* Reads from SQLite and syncs in batches
* Throws error if backfill fails
* No-op on Windows (Chroma disabled to prevent console popups)
*/
async ensureBackfilled(): Promise<void> {
if (this.disabled) return;
logger.info('CHROMA_SYNC', 'Starting smart backfill', { project: this.project });
await this.ensureCollection();
@@ -782,12 +816,17 @@ export class ChromaSync {
/**
* Query Chroma collection for semantic search
* Used by SearchManager for vector-based search
* Returns empty results on Windows (Chroma disabled to prevent console popups)
*/
async queryChroma(
query: string,
limit: number,
whereFilter?: Record<string, any>
): Promise<{ ids: number[]; distances: number[]; metadatas: any[] }> {
if (this.disabled) {
return { ids: [], distances: [], metadatas: [] };
}
await this.ensureConnection();
if (!this.client) {
+14 -2
View File
@@ -221,6 +221,16 @@ export class WorkerService {
// Start HTTP server FIRST - make port available immediately
await this.server.listen(port, host);
// Worker writes its own PID - reliable on all platforms
// This happens after listen() succeeds, ensuring the worker is actually ready
// On Windows, the spawner's PID is cmd.exe (useless), so worker must write its own
writePidFile({
pid: process.pid,
port,
startedAt: new Date().toISOString()
});
logger.info('SYSTEM', 'Worker started', { host, port, pid: process.pid });
// Do slow initialization in background (non-blocking)
@@ -482,7 +492,8 @@ async function main() {
exitWithStatus('error', 'Failed to spawn worker daemon');
}
writePidFile({ pid, port, startedAt: new Date().toISOString() });
// PID file is written by the worker itself after listen() succeeds
// This is race-free and works correctly on Windows where cmd.exe PID is useless
const healthy = await waitForHealth(port, getPlatformTimeout(30000));
if (!healthy) {
@@ -526,7 +537,8 @@ async function main() {
process.exit(0);
}
writePidFile({ pid, port, startedAt: new Date().toISOString() });
// PID file is written by the worker itself after listen() succeeds
// This is race-free and works correctly on Windows where cmd.exe PID is useless
const healthy = await waitForHealth(port, getPlatformTimeout(30000));
if (!healthy) {