Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 130abe04a9 | |||
| bff10d49c9 | |||
| 40a71d3250 | |||
| ae3d20c71a | |||
| 54ef9662c1 | |||
| 9aec461e14 |
@@ -10,7 +10,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.3.5",
|
"version": "7.3.7",
|
||||||
"source": "./plugin",
|
"source": "./plugin",
|
||||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [7.3.6] - 2025-12-17
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
- Enhanced SDKAgent response handling and message processing
|
||||||
|
|
||||||
|
## [7.3.5] - 2025-12-17
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* fix(windows): solve zombie port problem with wrapper architecture by @ToxMox in https://github.com/thedotmack/claude-mem/pull/372
|
||||||
|
* chore: bump version to 7.3.5 by @thedotmack in https://github.com/thedotmack/claude-mem/pull/375
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @ToxMox made their first contribution in https://github.com/thedotmack/claude-mem/pull/372
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.3.4...v7.3.5
|
||||||
|
|
||||||
## [7.3.4] - 2025-12-17
|
## [7.3.4] - 2025-12-17
|
||||||
|
|
||||||
Patch release for bug fixes and minor improvements
|
Patch release for bug fixes and minor improvements
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.3.5",
|
"version": "7.3.7",
|
||||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "7.3.5",
|
"version": "7.3.7",
|
||||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Alex Newman"
|
"name": "Alex Newman"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem-plugin",
|
"name": "claude-mem-plugin",
|
||||||
"version": "7.3.5",
|
"version": "7.3.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+26
-12
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+190
-252
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -6,12 +6,12 @@
|
|||||||
* native module dependencies.
|
* native module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
|
||||||
import { stdin } from "process";
|
import { stdin } from "process";
|
||||||
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
||||||
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
|
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
|
||||||
import { handleWorkerError } from "../shared/hook-error-handler.js";
|
import { handleWorkerError } from "../shared/hook-error-handler.js";
|
||||||
import { handleFetchError } from "./shared/error-handler.js";
|
import { handleFetchError } from "./shared/error-handler.js";
|
||||||
|
import { getProjectName } from "../utils/project-name.js";
|
||||||
|
|
||||||
export interface SessionStartInput {
|
export interface SessionStartInput {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -25,7 +25,7 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
|
|||||||
await ensureWorkerRunning();
|
await ensureWorkerRunning();
|
||||||
|
|
||||||
const cwd = input?.cwd ?? process.cwd();
|
const cwd = input?.cwd ?? process.cwd();
|
||||||
const project = cwd ? path.basename(cwd) : "unknown-project";
|
const project = getProjectName(cwd);
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
|
|
||||||
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
|
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import path from 'path';
|
|
||||||
import { stdin } from 'process';
|
import { stdin } from 'process';
|
||||||
import { createHookResponse } from './hook-response.js';
|
import { createHookResponse } from './hook-response.js';
|
||||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||||
import { handleFetchError } from './shared/error-handler.js';
|
import { handleFetchError } from './shared/error-handler.js';
|
||||||
|
import { getProjectName } from '../utils/project-name.js';
|
||||||
|
|
||||||
export interface UserPromptSubmitInput {
|
export interface UserPromptSubmitInput {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -24,7 +24,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { session_id, cwd, prompt } = input;
|
const { session_id, cwd, prompt } = input;
|
||||||
const project = path.basename(cwd);
|
const project = getProjectName(cwd);
|
||||||
|
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
toRelativePath,
|
toRelativePath,
|
||||||
extractFirstFile
|
extractFirstFile
|
||||||
} from '../shared/timeline-formatting.js';
|
} from '../shared/timeline-formatting.js';
|
||||||
|
import { getProjectName } from '../utils/project-name.js';
|
||||||
|
|
||||||
// Version marker path - use homedir-based path that works in both CJS and ESM contexts
|
// Version marker path - use homedir-based path that works in both CJS and ESM contexts
|
||||||
const VERSION_MARKER_PATH = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', '.install-version');
|
const VERSION_MARKER_PATH = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', '.install-version');
|
||||||
@@ -222,7 +223,7 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
|
|||||||
export async function generateContext(input?: ContextInput, useColors: boolean = false): Promise<string> {
|
export async function generateContext(input?: ContextInput, useColors: boolean = false): Promise<string> {
|
||||||
const config = loadContextConfig();
|
const config = loadContextConfig();
|
||||||
const cwd = input?.cwd ?? process.cwd();
|
const cwd = input?.cwd ?? process.cwd();
|
||||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
const project = getProjectName(cwd);
|
||||||
|
|
||||||
let db: SessionStore | null = null;
|
let db: SessionStore | null = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -271,29 +271,39 @@ export class ProcessManager {
|
|||||||
|
|
||||||
private static async waitForHealth(pid: number, port: number, timeoutMs: number = HEALTH_CHECK_TIMEOUT_MS): Promise<{ success: boolean; pid?: number; error?: string }> {
|
private static async waitForHealth(pid: number, port: number, timeoutMs: number = HEALTH_CHECK_TIMEOUT_MS): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
// Increase timeout on Windows to account for slower process startup
|
||||||
|
const adjustedTimeout = isWindows ? timeoutMs * 2 : timeoutMs;
|
||||||
|
|
||||||
while (Date.now() - startTime < timeoutMs) {
|
while (Date.now() - startTime < adjustedTimeout) {
|
||||||
// Check if process is still alive
|
// Check if process is still alive
|
||||||
if (!this.isProcessAlive(pid)) {
|
if (!this.isProcessAlive(pid)) {
|
||||||
return { success: false, error: 'Process died during startup' };
|
const errorMsg = isWindows
|
||||||
|
? `Process died during startup\n\nTroubleshooting:\n1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes\n2. Verify port ${port} is not in use: netstat -ano | findstr ${port}\n3. Check worker logs in ~/.claude-mem/logs/\n4. See GitHub issues: #363, #367, #371, #373\n5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`
|
||||||
|
: 'Process died during startup';
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try health check
|
// Try readiness check (changed from /health to /api/readiness)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
||||||
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
|
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return { success: true, pid };
|
return { success: true, pid };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not ready yet
|
// Not ready yet, continue polling
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Health check timed out' };
|
const timeoutMsg = isWindows
|
||||||
|
? `Worker failed to start on Windows (readiness check timed out after ${adjustedTimeout}ms)\n\nTroubleshooting:\n1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes\n2. Verify port ${port} is not in use: netstat -ano | findstr ${port}\n3. Check worker logs in ~/.claude-mem/logs/\n4. See GitHub issues: #363, #367, #371, #373\n5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`
|
||||||
|
: `Readiness check timed out after ${adjustedTimeout}ms`;
|
||||||
|
|
||||||
|
return { success: false, error: timeoutMsg };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async waitForExit(pid: number, timeout: number): Promise<void> {
|
private static async waitForExit(pid: number, timeout: number): Promise<void> {
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export class ChromaSync {
|
|||||||
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||||
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
||||||
this.transport = new StdioClientTransport({
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
|
const transportOptions: any = {
|
||||||
command: 'uvx',
|
command: 'uvx',
|
||||||
args: [
|
args: [
|
||||||
'--python', pythonVersion,
|
'--python', pythonVersion,
|
||||||
@@ -110,7 +112,16 @@ export class ChromaSync {
|
|||||||
'--data-dir', this.VECTOR_DB_DIR
|
'--data-dir', this.VECTOR_DB_DIR
|
||||||
],
|
],
|
||||||
stderr: 'ignore'
|
stderr: 'ignore'
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transport = new StdioClientTransport(transportOptions);
|
||||||
|
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
name: 'claude-mem-chroma-sync',
|
name: 'claude-mem-chroma-sync',
|
||||||
|
|||||||
+105
-23
@@ -14,7 +14,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import { exec } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -45,6 +45,10 @@ export class WorkerService {
|
|||||||
private startTime: number = Date.now();
|
private startTime: number = Date.now();
|
||||||
private mcpClient: Client;
|
private mcpClient: Client;
|
||||||
|
|
||||||
|
// Initialization flags for MCP/SDK readiness tracking
|
||||||
|
private mcpReady: boolean = false;
|
||||||
|
private initializationCompleteFlag: boolean = false;
|
||||||
|
|
||||||
// Domain services
|
// Domain services
|
||||||
private dbManager: DatabaseManager;
|
private dbManager: DatabaseManager;
|
||||||
private sessionManager: SessionManager;
|
private sessionManager: SessionManager;
|
||||||
@@ -128,17 +132,36 @@ export class WorkerService {
|
|||||||
hasIpc: typeof process.send === 'function',
|
hasIpc: typeof process.send === 'function',
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
|
initialized: this.initializationCompleteFlag,
|
||||||
|
mcpReady: this.mcpReady,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Readiness check endpoint - returns 503 until full initialization completes
|
||||||
|
// Used by ProcessManager and worker-utils to ensure worker is fully ready before routing requests
|
||||||
|
this.app.get('/api/readiness', (_req, res) => {
|
||||||
|
if (this.initializationCompleteFlag) {
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'ready',
|
||||||
|
mcpReady: this.mcpReady,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(503).json({
|
||||||
|
status: 'initializing',
|
||||||
|
message: 'Worker is still initializing, please retry',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Version endpoint - returns the worker's current version
|
// Version endpoint - returns the worker's current version
|
||||||
this.app.get('/api/version', (_req, res) => {
|
this.app.get('/api/version', (_req, res) => {
|
||||||
|
const { homedir } = require('os');
|
||||||
|
const { readFileSync } = require('fs');
|
||||||
|
const marketplaceRoot = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||||
|
const packageJsonPath = path.join(marketplaceRoot, 'package.json');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read version from marketplace package.json
|
// Read version from marketplace package.json
|
||||||
const { homedir } = require('os');
|
|
||||||
const { readFileSync } = require('fs');
|
|
||||||
const marketplaceRoot = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
|
||||||
const packageJsonPath = path.join(marketplaceRoot, 'package.json');
|
|
||||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||||
res.status(200).json({ version: packageJson.version });
|
res.status(200).json({ version: packageJson.version });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -295,25 +318,47 @@ export class WorkerService {
|
|||||||
*/
|
*/
|
||||||
private async cleanupOrphanedProcesses(): Promise<void> {
|
private async cleanupOrphanedProcesses(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Find all chroma-mcp processes
|
const isWindows = process.platform === 'win32';
|
||||||
const { stdout } = await execAsync('ps aux | grep "chroma-mcp" | grep -v grep || true');
|
|
||||||
|
|
||||||
if (!stdout.trim()) {
|
|
||||||
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = stdout.trim().split('\n');
|
|
||||||
const pids: number[] = [];
|
const pids: number[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
if (isWindows) {
|
||||||
const parts = line.trim().split(/\s+/);
|
// Windows: Use PowerShell Get-CimInstance to find chroma-mcp processes
|
||||||
if (parts.length > 1) {
|
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`;
|
||||||
const pid = parseInt(parts[1], 10);
|
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||||
if (!isNaN(pid)) {
|
|
||||||
|
if (!stdout.trim()) {
|
||||||
|
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Windows)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pidStrings = stdout.trim().split('\n');
|
||||||
|
for (const pidStr of pidStrings) {
|
||||||
|
const pid = parseInt(pidStr.trim(), 10);
|
||||||
|
// SECURITY: Validate PID is positive integer before adding to list
|
||||||
|
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
|
||||||
pids.push(pid);
|
pids.push(pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Unix: Use ps aux | grep
|
||||||
|
const { stdout } = await execAsync('ps aux | grep "chroma-mcp" | grep -v grep || true');
|
||||||
|
|
||||||
|
if (!stdout.trim()) {
|
||||||
|
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Unix)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = stdout.trim().split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const pid = parseInt(parts[1], 10);
|
||||||
|
// SECURITY: Validate PID is positive integer before adding to list
|
||||||
|
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
|
||||||
|
pids.push(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pids.length === 0) {
|
if (pids.length === 0) {
|
||||||
@@ -321,12 +366,28 @@ export class WorkerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
|
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
|
||||||
|
platform: isWindows ? 'Windows' : 'Unix',
|
||||||
count: pids.length,
|
count: pids.length,
|
||||||
pids
|
pids
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kill all found processes
|
// Kill all found processes
|
||||||
await execAsync(`kill ${pids.join(' ')}`);
|
if (isWindows) {
|
||||||
|
for (const pid of pids) {
|
||||||
|
// SECURITY: Double-check PID validation before using in taskkill command
|
||||||
|
if (!Number.isInteger(pid) || pid <= 0) {
|
||||||
|
logger.warn('SYSTEM', 'Skipping invalid PID', { pid });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 5000, stdio: 'ignore' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('SYSTEM', 'Failed to kill orphaned process', { pid }, error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await execAsync(`kill ${pids.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
|
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -380,7 +441,7 @@ export class WorkerService {
|
|||||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
||||||
|
|
||||||
// Connect to MCP server
|
// Connect to MCP server with timeout guard
|
||||||
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
command: 'node',
|
command: 'node',
|
||||||
@@ -388,10 +449,19 @@ export class WorkerService {
|
|||||||
env: process.env
|
env: process.env
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.mcpClient.connect(transport);
|
// Add timeout guard to prevent hanging on MCP connection (15 seconds)
|
||||||
|
const MCP_INIT_TIMEOUT_MS = 15000;
|
||||||
|
const mcpConnectionPromise = this.mcpClient.connect(transport);
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('MCP connection timeout after 15s')), MCP_INIT_TIMEOUT_MS)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.race([mcpConnectionPromise, timeoutPromise]);
|
||||||
|
this.mcpReady = true;
|
||||||
logger.success('WORKER', 'Connected to MCP server');
|
logger.success('WORKER', 'Connected to MCP server');
|
||||||
|
|
||||||
// Signal that initialization is complete
|
// Signal that initialization is complete
|
||||||
|
this.initializationCompleteFlag = true;
|
||||||
this.resolveInitialization();
|
this.resolveInitialization();
|
||||||
logger.info('SYSTEM', 'Background initialization complete');
|
logger.info('SYSTEM', 'Background initialization complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -492,6 +562,12 @@ export class WorkerService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SECURITY: Validate PID is a positive integer to prevent command injection
|
||||||
|
if (!Number.isInteger(parentPid) || parentPid <= 0) {
|
||||||
|
logger.warn('SYSTEM', 'Invalid parent PID for child process enumeration', { parentPid });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
|
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
|
||||||
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
const { stdout } = await execAsync(cmd, { timeout: 5000 });
|
||||||
@@ -499,7 +575,7 @@ export class WorkerService {
|
|||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(s => parseInt(s.trim(), 10))
|
.map(s => parseInt(s.trim(), 10))
|
||||||
.filter(n => !isNaN(n));
|
.filter(n => !isNaN(n) && Number.isInteger(n) && n > 0); // SECURITY: Validate each PID
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
|
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
|
||||||
return [];
|
return [];
|
||||||
@@ -510,6 +586,12 @@ export class WorkerService {
|
|||||||
* Force kill a process by PID (Windows: uses taskkill /F /T)
|
* Force kill a process by PID (Windows: uses taskkill /F /T)
|
||||||
*/
|
*/
|
||||||
private async forceKillProcess(pid: number): Promise<void> {
|
private async forceKillProcess(pid: number): Promise<void> {
|
||||||
|
// SECURITY: Validate PID is a positive integer to prevent command injection
|
||||||
|
if (!Number.isInteger(pid) || pid <= 0) {
|
||||||
|
logger.warn('SYSTEM', 'Invalid PID for force kill', { pid });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// /T kills entire process tree, /F forces termination
|
// /T kills entire process tree, /F forces termination
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export class SDKAgent {
|
|||||||
// Calculate discovery tokens (delta for this response only)
|
// Calculate discovery tokens (delta for this response only)
|
||||||
const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse;
|
const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse;
|
||||||
|
|
||||||
// Only log non-empty responses (filter out noise)
|
// Process response (empty or not) and mark messages as processed
|
||||||
if (responseSize > 0) {
|
if (responseSize > 0) {
|
||||||
const truncatedResponse = responseSize > 100
|
const truncatedResponse = responseSize > 100
|
||||||
? textContent.substring(0, 100) + '...'
|
? textContent.substring(0, 100) + '...'
|
||||||
@@ -125,6 +125,9 @@ export class SDKAgent {
|
|||||||
|
|
||||||
// Parse and process response with discovery token delta
|
// Parse and process response with discovery token delta
|
||||||
await this.processSDKResponse(session, textContent, worker, discoveryTokens);
|
await this.processSDKResponse(session, textContent, worker, discoveryTokens);
|
||||||
|
} else {
|
||||||
|
// Empty response - still need to mark pending messages as processed
|
||||||
|
await this.markMessagesProcessed(session, worker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,8 +399,15 @@ export class SDKAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: Mark ALL pending messages as successfully processed
|
// Mark messages as processed after successful observation/summary storage
|
||||||
// This prevents message loss if worker crashes before SDK finishes
|
await this.markMessagesProcessed(session, worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all pending messages as successfully processed
|
||||||
|
* CRITICAL: Prevents message loss and duplicate processing
|
||||||
|
*/
|
||||||
|
private async markMessagesProcessed(session: ActiveSession, worker: any | undefined): Promise<void> {
|
||||||
const pendingMessageStore = this.sessionManager.getPendingMessageStore();
|
const pendingMessageStore = this.sessionManager.getPendingMessageStore();
|
||||||
if (session.pendingProcessingIds.size > 0) {
|
if (session.pendingProcessingIds.size > 0) {
|
||||||
for (const messageId of session.pendingProcessingIds) {
|
for (const messageId of session.pendingProcessingIds) {
|
||||||
|
|||||||
@@ -58,17 +58,18 @@ export function getWorkerHost(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if worker is responsive by trying the health endpoint
|
* Check if worker is responsive and fully initialized by trying the readiness endpoint
|
||||||
|
* Changed from /health to /api/readiness to ensure MCP initialization is complete
|
||||||
*/
|
*/
|
||||||
async function isWorkerHealthy(): Promise<boolean> {
|
async function isWorkerHealthy(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const port = getWorkerPort();
|
const port = getWorkerPort();
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`, {
|
||||||
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug('SYSTEM', 'Worker health check failed', {
|
logger.debug('SYSTEM', 'Worker readiness check failed', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
errorType: error?.constructor?.name
|
errorType: error?.constructor?.name
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract project name from working directory path
|
||||||
|
* Handles edge cases: null/undefined cwd, drive roots, trailing slashes
|
||||||
|
*
|
||||||
|
* @param cwd - Current working directory (absolute path)
|
||||||
|
* @returns Project name or "unknown-project" if extraction fails
|
||||||
|
*/
|
||||||
|
export function getProjectName(cwd: string | null | undefined): string {
|
||||||
|
if (!cwd || cwd.trim() === '') {
|
||||||
|
logger.warn('PROJECT_NAME', 'Empty cwd provided, using fallback', { cwd });
|
||||||
|
return 'unknown-project';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract basename (handles trailing slashes automatically)
|
||||||
|
const basename = path.basename(cwd);
|
||||||
|
|
||||||
|
// Edge case: Drive roots on Windows (C:\, J:\) or Unix root (/)
|
||||||
|
// path.basename('C:\') returns '' (empty string)
|
||||||
|
if (basename === '') {
|
||||||
|
// Extract drive letter on Windows, or use 'root' on Unix
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
if (isWindows && cwd.match(/^[A-Z]:\\/i)) {
|
||||||
|
const driveLetter = cwd[0].toUpperCase();
|
||||||
|
const projectName = `drive-${driveLetter}`;
|
||||||
|
logger.info('PROJECT_NAME', 'Drive root detected', { cwd, projectName });
|
||||||
|
return projectName;
|
||||||
|
} else {
|
||||||
|
logger.warn('PROJECT_NAME', 'Root directory detected, using fallback', { cwd });
|
||||||
|
return 'unknown-project';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return basename;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user