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:
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
description: "Execute a plan using subagents for implementation"
|
||||||
|
argument-hint: "[task or plan reference]"
|
||||||
|
---
|
||||||
|
|
||||||
You are an ORCHESTRATOR.
|
You are an ORCHESTRATOR.
|
||||||
|
|
||||||
Primary instruction: deploy subagents to execute *all* work for #$ARGUMENTS.
|
Primary instruction: deploy subagents to execute *all* work for #$ARGUMENTS.
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
description: "Create an implementation plan with documentation discovery"
|
||||||
|
argument-hint: "[feature or task description]"
|
||||||
|
---
|
||||||
|
|
||||||
You are an ORCHESTRATOR.
|
You are an ORCHESTRATOR.
|
||||||
|
|
||||||
Create an LLM-friendly plan in phases that can be executed consecutively in new chat contexts.
|
Create an LLM-friendly plan in phases that can be executed consecutively in new chat contexts.
|
||||||
|
|||||||
+207
-214
File diff suppressed because one or more lines are too long
@@ -262,21 +262,55 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* Spawn a detached daemon process
|
* Spawn a detached daemon process
|
||||||
* Returns the child PID or undefined if spawn failed
|
* 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(
|
export function spawnDaemon(
|
||||||
scriptPath: string,
|
scriptPath: string,
|
||||||
port: number,
|
port: number,
|
||||||
extraEnv: Record<string, string> = {}
|
extraEnv: Record<string, string> = {}
|
||||||
): number | undefined {
|
): 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'], {
|
const child = spawn(process.execPath, [scriptPath, '--daemon'], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
windowsHide: true,
|
env
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
CLAUDE_MEM_WORKER_PORT: String(port),
|
|
||||||
...extraEnv
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (child.pid === undefined) {
|
if (child.pid === undefined) {
|
||||||
@@ -284,6 +318,7 @@ export function spawnDaemon(
|
|||||||
}
|
}
|
||||||
|
|
||||||
child.unref();
|
child.unref();
|
||||||
|
|
||||||
return child.pid;
|
return child.pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,10 +83,32 @@ export class ChromaSync {
|
|||||||
private readonly VECTOR_DB_DIR: string;
|
private readonly VECTOR_DB_DIR: string;
|
||||||
private readonly BATCH_SIZE = 100;
|
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) {
|
constructor(project: string) {
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.collectionName = `cm__${project}`;
|
this.collectionName = `cm__${project}`;
|
||||||
this.VECTOR_DB_DIR = path.join(os.homedir(), '.claude-mem', 'vector-db');
|
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
|
* Sync a single observation to Chroma
|
||||||
* Blocks until sync completes, throws on error
|
* Blocks until sync completes, throws on error
|
||||||
|
* No-op on Windows (Chroma disabled to prevent console popups)
|
||||||
*/
|
*/
|
||||||
async syncObservation(
|
async syncObservation(
|
||||||
observationId: number,
|
observationId: number,
|
||||||
@@ -397,6 +420,8 @@ export class ChromaSync {
|
|||||||
createdAtEpoch: number,
|
createdAtEpoch: number,
|
||||||
discoveryTokens: number = 0
|
discoveryTokens: number = 0
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
// Convert ParsedObservation to StoredObservation format
|
// Convert ParsedObservation to StoredObservation format
|
||||||
const stored: StoredObservation = {
|
const stored: StoredObservation = {
|
||||||
id: observationId,
|
id: observationId,
|
||||||
@@ -431,6 +456,7 @@ export class ChromaSync {
|
|||||||
/**
|
/**
|
||||||
* Sync a single summary to Chroma
|
* Sync a single summary to Chroma
|
||||||
* Blocks until sync completes, throws on error
|
* Blocks until sync completes, throws on error
|
||||||
|
* No-op on Windows (Chroma disabled to prevent console popups)
|
||||||
*/
|
*/
|
||||||
async syncSummary(
|
async syncSummary(
|
||||||
summaryId: number,
|
summaryId: number,
|
||||||
@@ -441,6 +467,8 @@ export class ChromaSync {
|
|||||||
createdAtEpoch: number,
|
createdAtEpoch: number,
|
||||||
discoveryTokens: number = 0
|
discoveryTokens: number = 0
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
// Convert ParsedSummary to StoredSummary format
|
// Convert ParsedSummary to StoredSummary format
|
||||||
const stored: StoredSummary = {
|
const stored: StoredSummary = {
|
||||||
id: summaryId,
|
id: summaryId,
|
||||||
@@ -491,6 +519,7 @@ export class ChromaSync {
|
|||||||
/**
|
/**
|
||||||
* Sync a single user prompt to Chroma
|
* Sync a single user prompt to Chroma
|
||||||
* Blocks until sync completes, throws on error
|
* Blocks until sync completes, throws on error
|
||||||
|
* No-op on Windows (Chroma disabled to prevent console popups)
|
||||||
*/
|
*/
|
||||||
async syncUserPrompt(
|
async syncUserPrompt(
|
||||||
promptId: number,
|
promptId: number,
|
||||||
@@ -500,6 +529,8 @@ export class ChromaSync {
|
|||||||
promptNumber: number,
|
promptNumber: number,
|
||||||
createdAtEpoch: number
|
createdAtEpoch: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
// Create StoredUserPrompt format
|
// Create StoredUserPrompt format
|
||||||
const stored: StoredUserPrompt = {
|
const stored: StoredUserPrompt = {
|
||||||
id: promptId,
|
id: promptId,
|
||||||
@@ -614,8 +645,11 @@ export class ChromaSync {
|
|||||||
* Backfill: Sync all observations missing from Chroma
|
* Backfill: Sync all observations missing from Chroma
|
||||||
* Reads from SQLite and syncs in batches
|
* Reads from SQLite and syncs in batches
|
||||||
* Throws error if backfill fails
|
* Throws error if backfill fails
|
||||||
|
* No-op on Windows (Chroma disabled to prevent console popups)
|
||||||
*/
|
*/
|
||||||
async ensureBackfilled(): Promise<void> {
|
async ensureBackfilled(): Promise<void> {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
logger.info('CHROMA_SYNC', 'Starting smart backfill', { project: this.project });
|
logger.info('CHROMA_SYNC', 'Starting smart backfill', { project: this.project });
|
||||||
|
|
||||||
await this.ensureCollection();
|
await this.ensureCollection();
|
||||||
@@ -782,12 +816,17 @@ export class ChromaSync {
|
|||||||
/**
|
/**
|
||||||
* Query Chroma collection for semantic search
|
* Query Chroma collection for semantic search
|
||||||
* Used by SearchManager for vector-based search
|
* Used by SearchManager for vector-based search
|
||||||
|
* Returns empty results on Windows (Chroma disabled to prevent console popups)
|
||||||
*/
|
*/
|
||||||
async queryChroma(
|
async queryChroma(
|
||||||
query: string,
|
query: string,
|
||||||
limit: number,
|
limit: number,
|
||||||
whereFilter?: Record<string, any>
|
whereFilter?: Record<string, any>
|
||||||
): Promise<{ ids: number[]; distances: number[]; metadatas: any[] }> {
|
): Promise<{ ids: number[]; distances: number[]; metadatas: any[] }> {
|
||||||
|
if (this.disabled) {
|
||||||
|
return { ids: [], distances: [], metadatas: [] };
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureConnection();
|
await this.ensureConnection();
|
||||||
|
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
|
|||||||
@@ -221,6 +221,16 @@ export class WorkerService {
|
|||||||
|
|
||||||
// Start HTTP server FIRST - make port available immediately
|
// Start HTTP server FIRST - make port available immediately
|
||||||
await this.server.listen(port, host);
|
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 });
|
logger.info('SYSTEM', 'Worker started', { host, port, pid: process.pid });
|
||||||
|
|
||||||
// Do slow initialization in background (non-blocking)
|
// Do slow initialization in background (non-blocking)
|
||||||
@@ -482,7 +492,8 @@ async function main() {
|
|||||||
exitWithStatus('error', 'Failed to spawn worker daemon');
|
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));
|
const healthy = await waitForHealth(port, getPlatformTimeout(30000));
|
||||||
if (!healthy) {
|
if (!healthy) {
|
||||||
@@ -526,7 +537,8 @@ async function main() {
|
|||||||
process.exit(0);
|
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));
|
const healthy = await waitForHealth(port, getPlatformTimeout(30000));
|
||||||
if (!healthy) {
|
if (!healthy) {
|
||||||
|
|||||||
Reference in New Issue
Block a user